reCAPTCHA v2 на странице логина — частая причина, по которой e2e-набор в CI зависает и падает по таймауту. Мокать проверку проще, но тогда тест перестаёт проверять реальный путь пользователя. Ниже — конвейер на pytest, Selenium и CaptchaAI: хелпер решает CAPTCHA по-настоящему и укладывается в обычный CI-таймаут GitHub Actions. Для QA-агентств с несколькими клиентами это ещё и предсказуемая смета — CaptchaAI тарифицирует по числу потоков в фиксированных долларах, а не по решению. Это удобно закладывать в счёт клиенту заранее, а не пересчитывать после каждого прогона очереди GitHub Actions.
Что понадобится:
- Python 3.11+ и pytest;
- Selenium с драйвером Chrome (headless-режим для CI);
- API-ключ CaptchaAI в переменной окружения
CAPTCHAAI_API_KEY; - staging-окружение с настоящей reCAPTCHA v2 на логине и чекауте — если CAPTCHA там отключена, тест не проверяет реальный путь пользователя.
Не мокайте reCAPTCHA v2 в e2e-тестах логина и чекаута ради «зелёного» прогона — тест начнёт проверять не тот путь, который видит настоящий пользователь.
Структура тестового репозитория
tests/
├── conftest.py # Shared fixtures
├── helpers/
│ ├── captcha.py # CaptchaAI integration
│ └── browser.py # Selenium helpers
├── test_login.py # Login flow tests
├── test_checkout.py # Checkout flow tests
└── pytest.ini # Config
Тесты логина и оформления заказа лежат отдельно от общих fixtures — так проще запускать pytest -m captcha только там, где реально стоит reCAPTCHA v2, и не тратить потоки CaptchaAI на остальной набор.
Хелпер CaptchaAI для e2e-тестов
# tests/helpers/captcha.py
import requests
import time
import os
class CaptchaTestHelper:
"""Solve CAPTCHAs during automated tests."""
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY required for CAPTCHA tests")
def solve_recaptcha(self, sitekey, pageurl):
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()
if result.get("status") != 1:
raise RuntimeError(f"Submit failed: {result.get('request')}")
task_id = result["request"]
time.sleep(15)
for _ in range(24):
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(data["request"])
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout in test")
def inject_token(self, driver, token):
"""Inject solved token into Selenium browser."""
driver.execute_script(
'document.getElementById("g-recaptcha-response").value = arguments[0];',
token,
)
# Trigger callback if available
driver.execute_script("""
if (typeof ___grecaptcha_cfg !== 'undefined') {
var clients = ___grecaptcha_cfg.clients;
for (var key in clients) {
var client = clients[key];
for (var prop in client) {
var val = client[prop];
if (val && typeof val === 'object') {
for (var inner in val) {
if (typeof val[inner] === 'function') {
val[inner](arguments[0]);
return;
}
}
}
}
}
}
""", token)
Хелпер шлёт sitekey/pageurl на in.php, ждёт 15 секунд и опрашивает res.php до 24 раз. Если ответа нет, тест падает по TimeoutError, а не зависает — это бережёт лимит минут CI-раннера.
Фикстуры pytest
Fixture captcha_solver живёт на весь сеанс — одного экземпляра CaptchaTestHelper хватает на все тесты модуля. browser, наоборот, пересоздаётся под каждый тест со scope="function", чтобы cookies и localStorage одного сценария не утекали в следующий.
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from helpers.captcha import CaptchaTestHelper
@pytest.fixture(scope="session")
def captcha_solver():
return CaptchaTestHelper()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope="session")
def base_url():
return "https://staging.example.com"
Тест логина с проверкой CAPTCHA
# tests/test_login.py
import pytest
from selenium.webdriver.common.by import By
class TestLogin:
def test_valid_login_with_captcha(self, browser, captcha_solver, base_url):
"""Test that login succeeds when CAPTCHA is solved correctly."""
browser.get(f"{base_url}/login")
# Fill form
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "password").send_keys("testpassword123")
# Solve CAPTCHA
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit
browser.find_element(By.ID, "login-btn").click()
# Assert redirect to dashboard
assert "/dashboard" in browser.current_url
assert browser.find_element(By.CLASS_NAME, "welcome-message")
def test_invalid_credentials_with_captcha(self, browser, captcha_solver, base_url):
"""Test that wrong credentials show error even with valid CAPTCHA."""
browser.get(f"{base_url}/login")
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "password").send_keys("wrongpass")
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
browser.find_element(By.ID, "login-btn").click()
error = browser.find_element(By.CLASS_NAME, "error-message")
assert "Invalid" in error.text
Оба теста запрашивают sitekey прямо из DOM перед вызовом solve_recaptcha, а не хардкодят его в коде — так набор переживёт ротацию sitekey на стороне продукта без правки самого файла с тестами.
Тест оформления заказа
# tests/test_checkout.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestCheckout:
def test_checkout_flow_with_captcha(self, browser, captcha_solver, base_url):
"""Full checkout flow: add item, fill form, solve CAPTCHA, confirm."""
# Add item to cart
browser.get(f"{base_url}/products/test-item")
browser.find_element(By.ID, "add-to-cart").click()
# Go to checkout
browser.get(f"{base_url}/checkout")
# Fill shipping
browser.find_element(By.ID, "address").send_keys("123 Test St")
browser.find_element(By.ID, "city").send_keys("Test City")
browser.find_element(By.ID, "zip").send_keys("12345")
# Solve CAPTCHA on checkout page
captcha_el = browser.find_element(By.CLASS_NAME, "g-recaptcha")
sitekey = captcha_el.get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit order
browser.find_element(By.ID, "place-order").click()
# Wait for confirmation
wait = WebDriverWait(browser, 15)
confirmation = wait.until(
EC.presence_of_element_located((By.CLASS_NAME, "order-confirmation"))
)
assert "Thank you" in confirmation.text
Чекаут решает CAPTCHA уже после заполнения формы доставки, а не до неё — так тест ближе к реальному пути пользователя и ловит сценарии, где форма валидируется раньше, чем проходит проверка CAPTCHA.
Настройка pytest.ini
# tests/pytest.ini
[pytest]
markers =
captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short
Маркер captcha — точка контроля бюджета: он явно помечает тесты, которые тратят потоки CaptchaAI, и позволяет исключить их флагом -m "not captcha" в быстром прогоне при каждом коммите, оставив полный набор с CAPTCHA для расписания.
CI-пайплайн в GitHub Actions
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6 AM
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pytest selenium requests
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: pytest tests/ -m captcha -v
Секция schedule запускает набор с CAPTCHA раз в неделю плюс на push в main — компромисс между частотой проверки и расходом потоков. Ежедневный или почасовой schedule для этого workflow обычно избыточен: reCAPTCHA v2 на логине и чекауте меняется редко, а лишние прогоны только сжигают потоки тарифа впустую.
Перед первым запуском в CI стоит проверить:
- секрет
CAPTCHAAI_API_KEYдобавлен в настройках репозитория, а не только в локальном.env; - staging-URL из
base_urlдоступен раннеру GitHub Actions (а не только из офисной сети); - версия Chrome в
browser-actions/setup-chromeне разъезжается с той, что используется локально.
Типичные проблемы и их решения
Ниже — проблемы, которые чаще всего всплывают при первом подключении CaptchaAI к CI, и что с ними делать без переписывания тестов.
| Проблема | Причина | Решение |
|---|---|---|
| Вставка токена в поле не сработала | Элемент textarea не найден |
Проверьте ID элемента или используйте querySelector('[name="g-recaptcha-response"]'). |
| Тесты проходят локально, но падают в CI | В раннере другая версия Chrome | Закрепите версию Chrome в настройке CI. |
| CAPTCHA не появляется в staging | В staging-окружении CAPTCHA отключена | Включите CAPTCHA в конфигурации staging-окружения. |
| Тайм-аут ожидания решения | Медленная сеть на раннере CI | Увеличьте тайм-аут опроса до 180 секунд. |
| API-ключ не найден в CI | Секрет CAPTCHAAI_API_KEY не прокинут в job |
Добавьте секрет репозитория в env: шага, как в примере workflow выше. |
| Токен пришёл, но форма его не приняла | На сайте нестандартный контейнер reCAPTCHA | Проверьте селектор g-recaptcha под конкретную страницу перед вызовом inject_token. |
Часто задаваемые вопросы
Сколько стоит прогон тестов с CAPTCHA в CI/CD?
Каждое решение — несколько центов, не фиксированная ставка за тест. Ежедневный набор из 10 тестов укладывается примерно в $10/месяц — сверх этого только тариф CaptchaAI (например, BASIC — $15/мес, 5 потоков). Гоняйте @pytest.mark.captcha по расписанию, а не на каждый коммит.
Сколько потоков CaptchaAI нужно для параллельного CI без очереди?
Ориентируйтесь на число одновременных Selenium-воркеров. Матрице до 5 job'ов хватает BASIC (5 потоков, $15/мес); для более широкой — STANDARD (15 потоков, $30/мес). Решения внутри потока не ограничены.
Тесты проходят локально, но падают по таймауту в CI — почему?
Обычно дело в сети раннера, а не в CAPTCHA. Поднимите тайм-аут опроса res.php (в примере — до 180 секунд) и закрепите ту же версию Chrome, что и локально.
Можно тем же хелпером решать не только reCAPTCHA v2, а, например, Cloudflare Turnstile?
Да — поменяйте method в запросе к in.php под нужный тип; CaptchaAI поддерживает Turnstile, GeeTest v3 и другие типы из матрицы. Структура хелпера (submit → poll → inject) остаётся той же.
Нужна ли настоящая reCAPTCHA v2 в staging, если в проде её отключают для внутренних пользователей?
Да, если обычные посетители продакшена всё равно видят CAPTCHA на логине или чекауте. Тест против окружения, где CAPTCHA выключена «для удобства», проверяет не тот путь, который проходят реальные пользователи, и не поймает регрессию в интеграции перед релизом.
Что делать, если токен решён, а колбэк reCAPTCHA всё равно не срабатывает?
Проверьте, что inject_token действительно нашёл клиент в ___grecaptcha_cfg и вызвал его функцию — если сайт рендерит reCAPTCHA нестандартно, структура clients может отличаться от ожидаемой, и токен просто не долетит до формы. На время отладки добавьте логирование внутри inject_token.
Связанные руководства
Не позволяйте CAPTCHA блокировать ваш конвейер тестов — начните с CaptchaAI.