Use Cases

Решение CAPTCHA для обеспечения качества и авторизованного тестирования

Нужно ли решать настоящую CAPTCHA в автотестах? Да — но только в E2E- и интеграционных сценариях, а не в юнит-тестах. CaptchaAI подключается к pytest и Selenium как обычная HTTP-зависимость: запрос на in.php, опрос res.php, готовый токен — и регрессия проходит в CI без ручного клика по виджету reCAPTCHA.

Правило одно: настоящий солвер — только там, где CAPTCHA действительно часть проверяемого потока (логин, регистрация, оформление заказа). Везде, где CAPTCHA просто мешает добраться до нужного экрана, дешевле и надёжнее подменить её моком.


Когда QA-инженерам нужно решение CAPTCHA

Сценарий Зачем нужен CaptchaAI
Регрессионное тестирование Форма логина или регистрации не сломалась после деплоя
Сквозные (E2E) сценарии Путь пользователя целиком — от логина до отправки формы
Нагрузочное тестирование Реальный поток CAPTCHA-запросов при параллельных сессиях
Кроссбраузерные проверки Виджет CAPTCHA рендерится одинаково в разных браузерах
Тестирование доступности Альтернативные сценарии для пользователей с ограниченными возможностями

Когда решение CAPTCHA НЕ нужно:

  • в юнит-тестах отдельных функций — подставляйте фиктивный токен;
  • в снэпшот-тестах статической вёрстки без реального сабмита формы;
  • при локальной отладке бизнес-логики без сети — используйте env-флаг из раздела про экономию ниже.

Хелпер для pytest: получаем токен reCAPTCHA v2 в фикстуре

Оберните вызов CaptchaAI в класс-хелпер и подключите через session-фикстуру — тест получает готовый токен, а интеграция с API живёт в одном месте.

Что делает хелпер

  • отправляет sitekey и pageurl на in.php и получает task_id;
  • опрашивает res.php с паузами, пока статус не станет 1;
  • поднимает RuntimeError при явной ошибке солвера и TimeoutError, если дедлайн истёк раньше, чем пришёл токен.
import pytest
import requests
import time


class CaptchaTestHelper:
    """Helper for solving CAPTCHAs in test environments."""

    def __init__(self, api_key):
        self.api_key = api_key

    def solve_recaptcha(self, sitekey, pageurl, timeout=120):
        """Solve reCAPTCHA and return token."""
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1,
        }, timeout=30)
        result = resp.json()
        assert result.get("status") == 1, f"Submit failed: {result}"
        task_id = result["request"]

        deadline = time.time() + timeout
        time.sleep(10)

        while time.time() < deadline:
            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": 1,
            }, timeout=15)
            data = resp.json()

            if data.get("status") == 1:
                return data["request"]
            if data["request"] != "CAPCHA_NOT_READY":
                raise RuntimeError(f"Solve error: {data['request']}")
            time.sleep(5)

        raise TimeoutError("CAPTCHA solve timeout")


@pytest.fixture(scope="session")
def captcha_helper():
    """Provide CaptchaAI helper for test session."""
    import os
    api_key = os.environ.get("CAPTCHAAI_API_KEY")
    if not api_key:
        pytest.skip("CAPTCHAAI_API_KEY not set")
    return CaptchaTestHelper(api_key)


class TestLoginFlow:
    """Test login flow behind reCAPTCHA."""

    SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
    LOGIN_URL = "https://staging.https://staging.example.com/qa-login"

    def test_login_with_valid_credentials(self, captcha_helper):
        """Verify login succeeds with valid creds and solved CAPTCHA."""
        token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
        assert token and len(token) > 100

        resp = requests.post(self.LOGIN_URL, data={
            "username": "test_user",
            "password": "test_pass",
            "g-recaptcha-response": token,
        })
        assert resp.status_code == 200
        assert "Welcome" in resp.text

    def test_login_with_invalid_credentials(self, captcha_helper):
        """Verify login fails gracefully with bad creds but valid CAPTCHA."""
        token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)

        resp = requests.post(self.LOGIN_URL, data={
            "username": "wrong_user",
            "password": "wrong_pass",
            "g-recaptcha-response": token,
        })
        assert resp.status_code in (200, 401)
        assert "Invalid" in resp.text or "error" in resp.text.lower()

    def test_login_without_captcha_fails(self):
        """Verify login rejects submissions without CAPTCHA."""
        resp = requests.post(self.LOGIN_URL, data={
            "username": "test_user",
            "password": "test_pass",
        })
        assert resp.status_code in (400, 403, 422)

E2E-тест на Selenium: регистрация с автоматическим решением CAPTCHA

Sitekey считывается прямо со страницы, токен подставляется в поле g-recaptcha-response, а колбэк reCAPTCHA вызывается вручную через execute_script — тест проходит форму так же, как пользователь после решения CAPTCHA.

Зачем вызывать data-callback вручную

Виджет reCAPTCHA v2 сам вызывает JS-функцию из атрибута data-callback, когда пользователь решает задачу в браузере. При программной инъекции токена этот вызов не срабатывает автоматически — форма может остаться заблокированной, даже если значение поля уже подставлено. Поэтому тест читает data-callback со страницы и вызывает функцию через execute_script сам, эмулируя то, что обычно делает виджет.

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def browser():
    """Create browser for testing."""
    options = webdriver.ChromeOptions()
    options.add_argument("--window-size=1920,1080")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


class TestRegistrationFlow:
    """Test registration form with CAPTCHA."""

    REG_URL = "https://staging.example.com/register"

    def test_registration_form_submits(self, browser, captcha_helper):
        """Full registration flow with CAPTCHA solving."""
        browser.get(self.REG_URL)

        # Fill form
        browser.find_element(By.ID, "email").send_keys("[email protected]")
        browser.find_element(By.ID, "password").send_keys("SecurePass123!")
        browser.find_element(By.ID, "confirm_password").send_keys("SecurePass123!")

        # Extract sitekey from page
        captcha_div = browser.find_element(By.CSS_SELECTOR, ".g-recaptcha")
        sitekey = captcha_div.get_attribute("data-sitekey")

        # Solve via API
        token = captcha_helper.solve_recaptcha(sitekey, browser.current_url)

        # Inject token
        browser.execute_script("""
            document.querySelector('[name="g-recaptcha-response"]').value = arguments[0];
        """, token)

        # Trigger callback if needed
        callback = captcha_div.get_attribute("data-callback")
        if callback:
            browser.execute_script(f"window['{callback}'](arguments[0]);", token)

        # Submit
        browser.find_element(By.CSS_SELECTOR, "button[type=submit]").click()

        # Verify success
        WebDriverWait(browser, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
        )

    def test_captcha_renders_on_page(self, browser):
        """Verify CAPTCHA widget loads on registration page."""
        browser.get(self.REG_URL)
        captcha = WebDriverWait(browser, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, iframe[src*='recaptcha']"))
        )
        assert captcha.is_displayed()

Как снизить расходы на CAPTCHA-тесты в CI

Приём Что это даёт
Гонять тесты против staging Там CAPTCHA обычно проще, чем в проде
Запускать по расписанию, а не на каждый push Меньше обращений к API
Кэшировать результат для flaky-тестов Не решать одну CAPTCHA повторно
Пропускать CAPTCHA локально через env-флаг Разработчик не ждёт решения
Выносить CAPTCHA-тесты в отдельный CI-джоб Проще считать расход по пайплайну

Команда QA со staging в европейском регионе для аудитории из России и Казахстана обычно укладывается в BASIC ($15/мес, 5 потоков). Для параллельных веток берите STANDARD ($30/мес, 15 потоков) — плата идёт за потоки, а не за число решённых CAPTCHA, поэтому рост числа тестов внутри спринта не увеличивает счёт сам по себе.

Если раннеры CI и staging разнесены географически (например, раннер в одном облачном регионе, а staging — в другом), закладывайте запас по таймауту сверх обычных 120 секунд: нестабильная сеть между регионами чаще ловит TimeoutError, чем медленный солвер.


Маркировка и выборочный запуск CAPTCHA-тестов

Самый простой способ вынести CAPTCHA-тесты в отдельный CI-джоб из таблицы выше — пометить их меткой captcha и гонять отдельно от быстрого пайплайна, а pre-commit — вообще без обращений к CaptchaAI.

Где это экономит время

  • pre-commit и pre-push хуки запускают только pytest -m "not captcha" — разработчик не ждёт ответа CaptchaAI на каждый коммит;
  • основной CI-джоб на pull request тоже пропускает CAPTCHA-тесты и падает за секунды, а не за минуты ожидания решения;
  • отдельный ночной или post-merge джоб гоняет pytest -m captcha целиком — именно там имеет смысл считать расход по потокам.
# conftest.py
import os

# Mark tests that need CAPTCHA solving
def pytest_configure(config):
    config.addinivalue_line(
        "markers", "captcha: tests requiring CAPTCHA solving (may be slow)"
    )


# pytest.ini or pyproject.toml
"""
[tool.pytest.ini_options]
markers = [
    "captcha: tests requiring CAPTCHA solving (may be slow)",
]
"""

Только тесты с CAPTCHA:

pytest -m captcha -v

Всё остальное — без обращений к CaptchaAI:

pytest -m "not captcha" -v

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

Сколько потоков CaptchaAI нужно для CI с параллельными джобами?

Поток берёт следующую CAPTCHA сразу после решения предыдущей. Для ночного прогона хватает BASIC (5 потоков), для параллельных веток — STANDARD (15 потоков) или ADVANCE ($90/мес, 50 потоков).

Сколько стоит прогон тестов с CAPTCHA?

CaptchaAI берёт плату за поток, а не за решение — внутри оплаченных потоков решения не ограничены. Набор из 50 CAPTCHA-тестов на BASIC укладывается в тот же поток без доплаты.

Нужно ли решать настоящую CAPTCHA в юнит-тестах?

Нет. В юнит-тестах подменяйте ответ CAPTCHA моком — реальное решение нужно только в интеграционных и E2E-сценариях.

Как не дать CAPTCHA-тесту зависнуть в CI по таймауту?

Задавайте единый дедлайн решения (120 секунд) и завершайте тест по TimeoutError, а не полагайтесь на общий таймаут CI-джоба.

Почему токен не срабатывает после инъекции в Selenium?

Скорее всего форма ждёт вызова data-callback, а не только заполненного поля g-recaptcha-response. Прочитайте атрибут data-callback с виджета и вызовите эту функцию через execute_script вручную, как в примере выше.


Связанные руководства


Автоматизируйте QA-конвейер — подключите CaptchaAI к своим тестам.

Комментарии для этой статьи отключены.