Use Cases

Обработка CAPTCHA при непрерывном интеграционном тестировании

Прогон падает на форме логина с 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.

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