Автоматизация OTP

Как автоматизировать OTP и SMS-2FA в тестах и скриптах

Одноразовые SMS-коды — тот шаг, на котором ломаются end-to-end-тесты. В этом руководстве показано, как оставить в тесте настоящий шаг: настоящая SIM в вашем телефоне и один вызов API, который ждёт код, — с рабочими примерами для Playwright, Selenium и Cypress.

$5 за подтверждённый номер в месяц · 7 дней бесплатно · оплата через Stripe на сайте

На этой странице

Почему OTP ломает автоматизацию

Тест умеет кликать, вводить текст и проверять результат, но не умеет читать телефон. Каждый привычный обходной путь чем-то жертвует:

  • Отключить 2FA на стейджинге — значит никогда не тестировать тот путь входа, которым на самом деле ходят пользователи.
  • Фиксированные тестовые коды работают только в системах, которые вы контролируете, и скрывают проблемы с доставкой.
  • Человек с телефоном не масштабируется на ночные прогоны и никогда не бывает на месте в 3 часа ночи.

Подход: настоящий номер, который читается через API

Вставьте свою SIM в Android-телефон (или используйте iPhone с автоматизацией в «Командах»), установите BusySMS и подтвердите номер. Зарегистрируйте тестовый аккаунт на этот номер. В тесте вызовите отправку SMS, запросите у API BusySMS следующий пришедший код и введите его. Код настоящий, путь доставки настоящий, и никому не нужно следить за телефоном.

  • Никаких устаревших кодов. Ожидающий запрос возвращает только код, пришедший после его начала, поэтому тест никогда не введёт код из прошлого прогона.
  • Один запрос. GET /api/v1/codes?timeoutMs=60000 работает в режиме long polling, так что цикл опроса писать не нужно.
  • Фильтры. phone, service и deviceId не дают параллельным тестам забирать чужие коды.

Разовая настройка

  1. 1

    Подключите телефон

    Установите BusySMS из Google Play на телефон с тестовой SIM, войдите и подтвердите номер. Подробности — в руководстве по SMS-шлюзу на Android.

  2. 2

    Зарегистрируйте тестовый аккаунт на этот номер

    Укажите номер в тестируемом продукте — ровно так, как это сделал бы пользователь.

  3. 3

    Создайте API-ключ

    Панель → REST API → «Создать ключ». Скопируйте его сразу: он показывается только один раз.

  4. 4

    Сохраните его как секрет CI

    Передайте его тест-раннеру как BUSYSMS_API_KEY. Никогда не коммитьте его и никогда не передавайте в тестируемый браузер.

Небольшой хелпер

Все примеры ниже построены на одной идее: начать ожидание, вызвать SMS, забрать код. Вынесите ожидание в один хелпер — на TypeScript или 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;
}

Начинайте ожидание до того, как нажмёте Отправить код. Так закрывается гонка, при которой быстрое SMS приходит раньше, чем вы начали ждать. Код, пришедший примерно за секунду до запроса, всё ещё считается новым, а более старый — нет.

Playwright

login.spec.ts
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)

Вызовы Selenium блокирующие, поэтому запустите ожидание в рабочем потоке, вызовите SMS, а затем заберите результат:

test_login.py
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

Команды Cypress выполняются в браузере, поэтому держите ключ подальше от него: вызывайте API из Node-задачи (task) в cypress.config.ts и используйте результат в тесте.

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
        },
      });
    },
  },
});

Cypress выполняет команды по порядку, поэтому здесь ожидание начинается сразу после клика. SMS редко доставляются так быстро, а код, пришедший за секунду до запроса, всё равно засчитывается.

Параллельные прогоны и нестабильная доставка

  • Самое простое правило — один номер на параллельный воркер. Если воркерам приходится делить номер, фильтруйте по service и выполняйте входы в один и тот же сервис по очереди.
  • Ожидание ограничено 90 секундами. Если SMS может идти дольше, повторите ожидание один раз, а не увеличивайте таймаут.
  • Если тест получил null, прежде чем винить продукт, проверьте телефон: GET /api/v1/numbers сообщает canReceive и reason, например gateway_off или phone_unreachable.
  • Если в момент начала ожидания ничего не перехватывается, BusySMS отправляет на телефон уведомление с просьбой снова включить шлюз. Нажать на него должен человек.
  • Перехваченные сообщения хранятся до 7 дней — достаточно, чтобы разобраться с упавшим ночным прогоном.

Обычные скрипты и CI-задачи

Ничто здесь не привязано к тестовому фреймворку. Shell-шаг в CI-задаче может сделать то же самое с curl и jq: запустить ожидание в фоне, вызвать SMS, затем прочитать результат.

bash
# 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"

jq выводит null, если код не пришёл, поэтому проверка валит задачу, а не вводит слово null в форму.

Используйте на своих аккаунтах

Это работает, потому что код приходит на ваш номер. Используйте это для аккаунтов, которыми вы управляете: тестовых пользователей вашего продукта, собственных админских входов, своего стейджинга. Управляете браузером через ИИ-агента, а не тест-раннер? Об этом — руководство по MCP.

BusySMS читает коды, отправленные на ваши собственные номера. Это не способ обойти чужую проверку, и он не предназначен для массового создания аккаунтов.

Вопросы и ответы

Частые вопросы

Как автоматизировать проверку OTP в Selenium?

Запустите GET /api/v1/codes?timeoutMs=60000 в рабочем потоке, вызовите SMS, затем введите полученный код в поле. Пример на Python выше делает именно это.

Можно ли автоматизировать OTP без настоящего телефона?

С BusySMS — нет. Коды приходят с вашей SIM в настоящем телефоне; эмуляторы в роли шлюза не поддерживаются.

Может ли тест получить старый код?

Нет. Если timeoutMs больше нуля, API возвращает только коды, пришедшие после начала запроса, а если таких нет — null.

Сколько стоит запуск в CI?

$5 за подтверждённый номер в месяц после бесплатного пробного периода на 7 дней. Цена — за номер, а не за тестовый прогон или код.

Работает ли это с кодами из e-mail или приложений-аутентификаторов?

Нет. BusySMS читает коды, отправленные по SMS на ваши собственные номера. Коды из e-mail и приложений-аутентификаторов — не SMS, поэтому они вне его задач.

Может ли вся команда пользоваться одним тестовым номером?

Да. Поделитесь номером максимум с пятью другими пользователями BusySMS; после принятия приглашения их собственные API-ключи тоже получают новые коды этого номера.

Блог

Ещё в блоге

Все статьи

Читайте также

Похожие руководства

Все руководства

Попробуйте на своём номере

Установите приложение, подтвердите номер и получите первый код уже сегодня. Каждый аккаунт начинается с бесплатного пробного периода на 7 дней; дальше — $5 за подтверждённый номер в месяц, отменить можно в любой момент.