Нужно ли решать настоящую 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 к своим тестам.