Прогон падает на форме логина с reCAPTCHA, хотя локально всё зелёное, — знакомая картина для любой команды, которая гоняет e2e-тесты в GitHub Actions, GitLab CI или Jenkins. CAPTCHA рассчитана на живого человека, а раннер CI — это headless Chrome без пользователя за клавиатурой. Ниже — рабочая схема: helper на Python, который решает CAPTCHA через API CaptchaAI, и готовые конфиги для трёх популярных CI-систем.
Почему тесты падают на CAPTCHA и как это обойти
Пайплайн CI/CD запускается по триггеру — пуш, PR, ночное расписание — без человека рядом. CAPTCHA, наоборот, спроектирована так, чтобы отсеивать именно автоматические запросы. Итог предсказуем: без службы решения любой e2e-сценарий стабильно падает на этом шаге, а не изредка. Чаще всего это бьёт по одним и тем же экранам:
- форма логина с reCAPTCHA v2 или v3;
- регистрация нового аккаунта с Cloudflare Turnstile;
- форма обратной связи или чекаут с любой другой защитой.
Практическое решение — вызывать API CaptchaAI прямо из тестового набора. API-ключ хранится как секрет CI (GitHub Secrets, GitLab CI Variables, Jenkins Credentials — без исключений), а сам тест на лету получает токен и подставляет его в форму до клика по кнопке отправки. Раннер продолжает работу так, будто CAPTCHA вообще не было.
Архитектура
┌──────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Git Push │────▶│ CI Runner │────▶│ E2E Tests │────▶│ Test Report │
│ │ │ (headless │ │ + CAPTCHA │ │ │
│ │ │ Chrome) │ │ solving │ │ │
└──────────────┘ └──────────────┘ └────────────┘ └──────────────┘
│
▼
┌────────────┐
│ CaptchaAI │
│ API │
└────────────┘
Раннер поднимает headless Chrome, e2e-тест доходит до формы с CAPTCHA и вместо того, чтобы упасть, дергает API CaptchaAI, получает токен и подставляет его в скрытое поле формы — отчёт о тестах формируется как обычно.
CAPTCHA-хелпер для тестового набора
Ниже — минимальный класс-обёртка над API CaptchaAI: отправляет задачу в in.php, опрашивает res.php с интервалом и возвращает готовый токен или кидает исключение, если время вышло. Этот же класс переиспользуется во всех примерах CI ниже.
import os
import time
import requests
class CICaptchaSolver:
"""CAPTCHA solver designed for CI environments."""
BASE = "https://ocr.captchaai.com"
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY not set")
def solve(self, params, initial_wait=10, timeout=120):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(f"CAPTCHA submit failed: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
deadline = time.time() + timeout
while time.time() < deadline:
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(f"CAPTCHA solve failed: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out in CI")
def solve_recaptcha(self, sitekey, pageurl):
return self.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
})
def solve_turnstile(self, sitekey, pageurl):
return self.solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
Интеграция с pytest
Хелпер подключается через фикстуру сессии, а Selenium — через фикстуру функции, чтобы каждый тест получал чистый браузер и общий на всю сессию объект CICaptchaSolver.
conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="session")
def captcha_solver():
return CICaptchaSolver()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=options)
driver.set_window_size(1920, 1080)
yield driver
driver.quit()
Файл с тестами
Пример ниже покрывает форму логина (успешный сценарий и неверный пароль) и форму обратной связи с Cloudflare Turnstile — токен из хелпера подставляется в скрытое поле напрямую через execute_script, до клика по кнопке отправки.
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestLoginFlow:
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
LOGIN_URL = "https://staging.https://staging.example.com/qa-login"
def test_login_with_captcha(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
# Fill credentials
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("testpass123")
# Solve CAPTCHA
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
# Submit
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
# Verify login success
assert "dashboard" in browser.current_url.lower()
def test_login_wrong_password(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("wrongpass")
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
error = browser.find_element(By.CSS_SELECTOR, ".error-message")
assert error.is_displayed()
class TestContactForm:
SITEKEY = "0x4AAAA..."
FORM_URL = "https://staging.example.com/contact"
def test_contact_form_submission(self, browser, captcha_solver):
browser.get(self.FORM_URL)
browser.find_element(By.ID, "name").send_keys("CI Test")
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "message").send_keys("Automated CI test")
token = captcha_solver.solve_turnstile(self.SITEKEY, self.FORM_URL)
browser.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
browser.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
)
Конфиги для трёх CI-систем
Логика во всех трёх одна и та же: поднять headless Chrome, прогнать pytest tests/e2e/, а ключ CaptchaAI подать через встроенное хранилище секретов конкретной платформы. Различаются только детали окружения.
GitHub Actions
Ключ лежит в secrets.CAPTCHAAI_API_KEY, а workflow ставит Chrome и ChromeDriver перед прогоном pytest и всегда сохраняет HTML-отчёт как артефакт, даже если тесты упали.
name: E2E Tests with CAPTCHA
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: Install ChromeDriver
uses: nanasess/setup-chromedriver@v2
- name: Install dependencies
run: |
pip install selenium requests pytest pytest-html
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: |
pytest tests/e2e/ -v --html=report.html --self-contained-html
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-report
path: report.html
GitLab CI
Тот же принцип, но браузер поднимается отдельным сервисом selenium/standalone-chrome, а тесты подключаются к нему по SELENIUM_REMOTE_URL вместо локального ChromeDriver.
e2e_tests:
stage: test
image: python:3.11
services:
- selenium/standalone-chrome:latest
variables:
SELENIUM_REMOTE_URL: "http://selenium__standalone-chrome:4444/wd/hub"
script:
- pip install selenium requests pytest
- pytest tests/e2e/ -v
artifacts:
when: always
reports:
junit: report.xml
Jenkins
Ключ API берётся из Jenkins Credentials через credentials('captchaai-api-key') — так секрет не попадает ни в лог, ни в конфиг pipeline.
pipeline {
agent any
environment {
CAPTCHAAI_API_KEY = credentials('captchaai-api-key')
}
stages {
stage('Setup') {
steps {
sh 'pip install selenium requests pytest'
}
}
stage('E2E Tests') {
steps {
sh 'pytest tests/e2e/ -v --junitxml=results.xml'
}
}
}
post {
always {
junit 'results.xml'
}
}
}
Сколько это стоит и как не переплачивать
CaptchaAI тарифицируется по потокам, а не по числу решённых CAPTCHA, и это удобно считать заранее. Возьмём типичный набор: 20 e2e-сценариев, каждый упирается в reCAPTCHA v2 на логине. Если pytest гоняет их последовательно, хватает плана BASIC ($15/мес, 5 потоков) — очередь из 20 задач просто отработает по 5 штук за раз. Если вы запускаете pytest-xdist с 10 воркерами, чтобы уложить прогон в разумное время, под параллельную нагрузку нужен план побольше — например ADVANCE ($90/мес, 50 потоков) с запасом. Оплата в USD и без скрытых наценок за тип CAPTCHA — это особенно ощутимо для команд, которые выставляют счета в валюте, отличной от доллара, и не хотят пересчитывать смету каждый месяц.
Дальше — два приёма, которые реально снижают счёт: не гонять CAPTCHA-тесты там, где они не нужны, и проверять баланс до старта набора, а не после падения половины job'ов.
Решайте только тогда, когда это необходимо
import os
def should_run_captcha_tests():
"""Skip CAPTCHA tests in certain environments."""
if os.environ.get("SKIP_CAPTCHA_TESTS"):
return False
if not os.environ.get("CAPTCHAAI_API_KEY"):
return False
return True
# In test
import pytest
@pytest.mark.skipif(
not should_run_captcha_tests(),
reason="CAPTCHA tests disabled or API key not set"
)
class TestWithCaptcha:
def test_login(self, browser, captcha_solver):
pass
Проверяйте баланс до старта, а не после падения тестов
Фикстура ниже с autouse=True дергает getbalance перед первым тестом сессии и пропускает весь набор, если баланс ниже порога — это дешевле, чем разбираться, почему упали 15 job'ов подряд из-за нулевого счёта на аккаунте CaptchaAI.
@pytest.fixture(scope="session", autouse=True)
def check_captcha_balance(captcha_solver):
import requests
resp = requests.get(
f"{captcha_solver.BASE}/res.php",
params={"key": captcha_solver.api_key, "action": "getbalance"},
)
balance = float(resp.text)
if balance < 0.50:
pytest.skip(f"CaptchaAI balance too low: ${balance:.2f}")
Типичные проблемы и их решения
Пять ситуаций встречаются в этой связке чаще всего — от незаданного секрета до дорогого прогона на каждый PR.
| Проблема | Причина | Что сделать |
|---|---|---|
CAPTCHAAI_API_KEY not set |
Секрет не добавлен в настройки CI | Добавьте ключ в секреты CI-платформы |
| Chrome падает в CI | Не передан флаг --no-sandbox |
Добавьте флаги headless-режима Chrome, как в примере conftest.py |
| Локально тесты зелёные, в CI падают | В CI другая версия Chrome | Закрепите версию Chrome явно (chrome-version: stable или конкретный номер) |
| Истекло время ожидания CAPTCHA | Сеть CI-раннера медленная или раннер географически далеко от целевого сайта | Увеличьте timeout и initial_wait в CICaptchaSolver |
| Прогон обходится дорого | Слишком много решений CAPTCHA на каждый PR | Пропускайте CAPTCHA-тесты флагом SKIP_CAPTCHA_TESTS для PR-сборок |
Часто задаваемые вопросы
Нужно ли решать CAPTCHA в каждом прогоне CI?
Нет — держите CAPTCHA-тесты для мерджа в main или для ночного расписания, а на каждый PR пропускайте их флагом SKIP_CAPTCHA_TESTS. Так вы экономите и потоки CaptchaAI, и время самого прогона.
Какой тариф CaptchaAI выбрать для CI-пайплайна?
Для последовательного набора из 10–20 сценариев хватает BASIC ($15/мес, 5 потоков). Если тесты гоняются параллельно через pytest-xdist на несколько воркеров, берите план с запасом потоков под число воркеров — например ADVANCE ($90/мес, 50 потоков). Уточняйте актуальные тарифы на сайте CaptchaAI перед тем, как закладывать их в бюджет CI.
Тесты стабильно падают по таймауту на CAPTCHA — в чём дело?
Чаще всего дело не в CaptchaAI, а в сети CI-раннера: DNS резолвится медленно или раннер физически далеко от целевого домена, и initial_wait в 10 секунд оказывается слишком коротким. Увеличьте initial_wait и timeout в хелпере и заложите запас на RTT, если раннеры расположены не рядом с сайтом.
Как правильно хранить API-ключ CaptchaAI в CI?
Через встроенное хранилище секретов вашей CI-платформы — GitHub Secrets, GitLab CI Variables или Jenkins Credentials — и никогда напрямую в коде теста или в переменных репозитория. Ключ не должен попадать ни в git, ни в лог сборки.
Нужны ли реальные sitekey целевой страницы или подойдут тестовые?
Используйте реальный sitekey целевой страницы — CaptchaAI решает CAPTCHA конкретного сайта, а не абстрактную заглушку. При этом сама страница вполне может быть staging-окружением вашей команды: держите тесты на staging-домене и передавайте в форму только те данные, которые ваша QA-среда вправе обрабатывать.
Связанные руководства
Если хелпер из этой статьи ещё нужно встроить в саму регистрацию, а не только в логин, или разобраться с форматом ответа API подробнее — начните здесь:
Подключите решение CAPTCHA к своему CI — начните работу с CaptchaAI.