Nesta página
Por que o OTP quebra a automação
Um teste consegue clicar, digitar e verificar, mas não consegue ler um celular. Cada uma das gambiarras de sempre abre mão de alguma coisa:
- Desligar o 2FA em staging significa que o caminho de login que os usuários realmente fazem nunca é testado.
- Códigos de teste fixos só funcionam em sistemas que você controla, e escondem problemas de entrega.
- Uma pessoa com o celular não escala para uma execução noturna, e nunca está lá às 3 da manhã.
A abordagem: um número real, lido por API
Coloque um chip seu em um celular Android (ou use um iPhone com uma automação do Atalhos), instale o BusySMS e verifique o número. Cadastre a sua conta de teste nesse número. No teste, dispare o SMS, peça à API do BusySMS o próximo código que chegar e digite-o. O código é real, o caminho de entrega é real, e ninguém precisa ficar de olho no celular.
- Sem códigos velhos. Uma requisição em espera só retorna um código que chegou depois que ela começou, então um teste nunca envia o código da execução anterior.
- Uma requisição.
GET /api/v1/codes?timeoutMs=60000faz long-polling, então não há loop de polling para escrever. - Filtros.
phone,serviceedeviceIdimpedem que testes em paralelo peguem os códigos uns dos outros.
Configuração única
- 1
Cadastre um celular
Instale o BusySMS pelo Google Play em um celular com o chip de teste, entre e verifique o número. O guia do gateway de SMS no Android tem os detalhes.
- 2
Cadastre a conta de teste nesse número
Use o número no produto que você está testando, exatamente como um usuário faria.
- 3
Crie uma chave de API
Painel → API REST → Criar chave. Copie na hora: ela só aparece uma vez.
- 4
Guarde-a como segredo de CI
Exponha-a ao executor de testes como
BUSYSMS_API_KEY. Nunca faça commit dela, e nunca a envie para o navegador sob teste.
Uma pequena função auxiliar
Todos os exemplos abaixo usam a mesma ideia: comece a esperar, dispare o SMS, pegue o código. Coloque a espera em uma função auxiliar, em TypeScript ou Python:
const API = "https://busysms.net/api/v1/codes";
export async function waitForSmsCode(
opts: { phone?: string; service?: string; timeoutMs?: number } = {},
): Promise<string> {
const params = new URLSearchParams({ timeoutMs: String(opts.timeoutMs ?? 60_000) });
if (opts.phone) params.set("phone", opts.phone);
if (opts.service) params.set("service", opts.service);
const res = await fetch(`${API}?${params}`, {
headers: { Authorization: `Bearer ${process.env.BUSYSMS_API_KEY}` },
});
if (!res.ok) throw new Error(`BusySMS ${res.status}: ${await res.text()}`);
const { code } = (await res.json()) as { code: string | null };
if (!code) throw new Error("No new SMS code arrived in time");
return code;
}import os
import requests
API = "https://busysms.net/api/v1/codes"
def wait_for_sms_code(phone=None, service=None, timeout_ms=60_000):
params = {"timeoutMs": timeout_ms}
if phone:
params["phone"] = phone
if service:
params["service"] = service
r = requests.get(
API,
params=params,
headers={"Authorization": f"Bearer {os.environ['BUSYSMS_API_KEY']}"},
timeout=timeout_ms / 1000 + 15,
)
r.raise_for_status()
code = r.json()["code"]
if not code:
raise TimeoutError("No new SMS code arrived in time")
return codeComece a espera antes de clicar em Enviar código. Isso fecha a condição de corrida em que um SMS rápido chega antes de você começar a esperar. Um código que chegou até cerca de um segundo antes da requisição ainda é tratado como novo, mas qualquer um mais antigo, não.
Playwright
import { test, expect } from "@playwright/test";
import { waitForSmsCode } from "./busysms";
test("signs in with SMS two-factor", async ({ page }) => {
test.setTimeout(120_000); // the default 30 s is shorter than an SMS wait
await page.goto("https://staging.example.com/login");
await page.getByLabel("Phone number").fill("+44 7700 900123");
// Start waiting BEFORE the SMS is sent, then trigger it.
const code = waitForSmsCode({ phone: "447700900123" });
await page.getByRole("button", { name: "Send code" }).click();
await page.getByLabel("Verification code").fill(await code);
await page.getByRole("button", { name: "Verify" }).click();
await expect(page).toHaveURL(/dashboard/);
});Selenium (Python)
As chamadas do Selenium são bloqueantes, então rode a espera em uma thread separada, dispare o SMS e depois colete o resultado:
from concurrent.futures import ThreadPoolExecutor
from selenium import webdriver
from selenium.webdriver.common.by import By
from busysms import wait_for_sms_code
driver = webdriver.Chrome()
driver.get("https://staging.example.com/login")
driver.find_element(By.NAME, "phone").send_keys("+44 7700 900123")
with ThreadPoolExecutor(max_workers=1) as pool:
# Start waiting first, then trigger the SMS.
pending = pool.submit(wait_for_sms_code, phone="447700900123")
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
code = pending.result()
driver.find_element(By.NAME, "otp").send_keys(code)
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()Cypress
Os comandos do Cypress rodam no navegador, então mantenha a chave fora dele: chame a API a partir de uma task do Node em cypress.config.ts e use o resultado no teste.
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on("task", {
// Runs in Node, so the API key never reaches the browser.
async waitForSmsCode({ phone }: { phone: string }) {
const params = new URLSearchParams({ phone, timeoutMs: "60000" });
const res = await fetch(`https://busysms.net/api/v1/codes?${params}`, {
headers: { Authorization: `Bearer ${process.env.BUSYSMS_API_KEY}` },
});
if (!res.ok) throw new Error(`BusySMS ${res.status}`);
const { code } = await res.json();
return code; // null if nothing new arrived
},
});
},
},
});it("signs in with SMS two-factor", () => {
cy.visit("https://staging.example.com/login");
cy.get("input[name=phone]").type("+44 7700 900123");
cy.contains("button", "Send code").click();
cy.task("waitForSmsCode", { phone: "447700900123" }, { timeout: 90_000 }).then((code) => {
expect(code, "SMS code").to.be.a("string");
cy.get("input[name=otp]").type(String(code));
});
cy.contains("button", "Verify").click();
});O Cypress executa os comandos em ordem, então aqui a espera começa logo depois do clique. A entrega de SMS raramente é tão rápida, e um código que chegou até um segundo antes da requisição ainda conta.
Execuções em paralelo e entrega instável
- Um número por worker em paralelo é a regra mais simples. Se os workers precisarem compartilhar um número, filtre por
servicee rode os logins no mesmo serviço um de cada vez. - Uma espera tem limite de 90 segundos. Se um SMS pode demorar mais, repita a espera uma vez em vez de aumentar o timeout.
- Quando um teste recebe
null, confira o celular antes de culpar o produto:GET /api/v1/numbersinformacanReceivee umreasoncomogateway_offouphone_unreachable. - Se nada estiver capturando quando uma espera começar, o BusySMS manda uma notificação para o celular pedindo que o gateway seja ligado de novo. Alguém precisa tocar nela.
- As mensagens capturadas ficam guardadas por até 7 dias, tempo suficiente para depurar a execução que falhou ontem à noite.
Scripts simples e jobs de CI
Nada aqui é específico de um framework de testes. Um passo de shell em um job de CI faz o mesmo com curl e jq: comece a espera em segundo plano, dispare o SMS e depois leia o resultado.
# Start waiting in the background, then trigger the SMS
curl -s -H "Authorization: Bearer $BUSYSMS_API_KEY" \
"https://busysms.net/api/v1/codes?phone=447700900123&timeoutMs=60000" > code.json &
WAIT_PID=$!
./trigger-login.sh # whatever makes the service send the SMS
wait "$WAIT_PID"
CODE=$(jq -r .code code.json)
[ "$CODE" != "null" ] || { echo "No SMS code arrived" >&2; exit 1; }
echo "Got code $CODE"O jq imprime null quando nenhum código chegou, então a checagem faz o job falhar em vez de digitar a palavra null em um formulário.
Use em contas que são suas
Isso funciona porque o código é enviado para um número que é seu. Use em contas que você controla: os usuários de teste do seu produto, seus próprios logins de administrador, o seu ambiente de staging. Vai controlar o navegador com um agente de IA em vez de um executor de testes? O guia de MCP explica como.
O BusySMS lê códigos enviados aos seus próprios números. Ele não é um jeito de burlar a verificação de outra pessoa, e não foi feito para criar contas em massa.
Dúvidas
Perguntas frequentes
Como automatizo a verificação por OTP no Selenium?
Inicie GET /api/v1/codes?timeoutMs=60000 em uma thread separada, dispare o SMS e digite o código retornado no campo. O exemplo em Python acima faz exatamente isso.
Posso automatizar OTP sem um celular de verdade?
Com o BusySMS, não. Os códigos vêm de um chip seu em um celular de verdade; emuladores não são aceitos como gateway.
Um teste pode pegar um código antigo?
Não. Com timeoutMs acima de zero, a API só retorna códigos que chegaram depois do início da requisição, e retorna null se nenhum chegou.
Quanto custa rodar no CI?
$5 por número verificado ao mês depois de 7 dias grátis. O preço é por número, não por execução de teste nem por código.
Funciona com códigos por e-mail ou apps autenticadores?
Não. O BusySMS lê códigos enviados por SMS para os seus próprios números. Códigos por e-mail e de apps autenticadores não são SMS, então ficam de fora.
Minha equipe toda pode usar um único número de teste?
Sim. Compartilhe o número com até cinco outros usuários do BusySMS; depois que eles aceitarem, as próprias chaves de API deles também recebem os códigos novos desse número.
Blog
Mais do blog
Todos os artigosContinue lendo
Guias relacionados
Todos os guiasAPI de gateway de SMS
A referência da API de recebimento: autenticação, endpoints, formato da resposta, erros e código em curl, Node.js e Python.
Ler o guiaCódigos SMS para agentes de IA
Deixe o Claude, o Codex ou qualquer cliente MCP esperar códigos de verificação SMS pelo servidor MCP do BusySMS.
Ler o guiaGateway de SMS no Android
Transforme um celular Android seu em um gateway cujos códigos de verificação você lê por API, MCP, painel ou Chrome.
Ler o guiaTeste com um número que é seu
Instale o app, verifique seu número e leia seu primeiro código hoje mesmo. Toda conta começa com 7 dias grátis; depois disso, são $5 por número verificado ao mês, e você pode cancelar quando quiser.
