Casos de Uso

Manejo de CAPTCHA en pruebas de integración continua

Guarda tu API key de CaptchaAI como secreto de CI y deja que la suite resuelva cada reCAPTCHA v2 o Turnstile sobre la marcha: tus pruebas end-to-end vuelven a pasar sin que nadie toque el navegador.

Por qué los CAPTCHA rompen un pipeline de CI/CD

Un pipeline de CI/CD corre solo, sin nadie delante del teclado. El CAPTCHA está diseñado para lo contrario: exigir un gesto humano. Ese choque tiene una consecuencia muy concreta: cada vez que una prueba end-to-end llega a un login o a un formulario protegido, se queda clavada y el job termina en rojo. Para un equipo que hace deploy varias veces al día, eso es una suite E2E que nadie se fía de dejar en verde.

La salida es tratar el CAPTCHA como una dependencia externa más de la prueba. Son tres piezas que encajan en cualquier plataforma de CI:

  • Un helper que envía el CAPTCHA a la API de CaptchaAI y espera el token.
  • La API key guardada como secreto del CI, nunca en el repositorio.
  • La prueba, que inyecta el token en el formulario antes de enviarlo.

Con eso, el navegador headless nunca tiene que "resolver" el desafío a mano: la prueba lo hace por su cuenta durante la ejecución del pipeline.

Cómo encaja la resolución en el flujo

El runner de CI arranca un Chrome headless, ejecuta los tests y, cuando aparece un CAPTCHA, delega la resolución en la API de CaptchaAI. El token vuelve a la prueba y el flujo continúa hasta el reporte final:

┌──────────────┐     ┌──────────────┐     ┌────────────┐     ┌──────────────┐
│ Git Push     │────▶│ CI Runner    │────▶│ E2E Tests  │────▶│ Test Report  │
│              │     │ (headless    │     │ + CAPTCHA  │     │              │
│              │     │  Chrome)     │     │ solving    │     │              │
└──────────────┘     └──────────────┘     └────────────┘     └──────────────┘
                                                │
                                                ▼
                                         ┌────────────┐
                                         │ CaptchaAI  │
                                         │ API        │
                                         └────────────┘

Un helper de resolución pensado para CI

Encapsula la lógica de envío y sondeo en una clase reutilizable. Lee la API key del entorno (nunca la escribas en el código), envía la tarea al endpoint in.php y consulta el resultado en res.php hasta que esté listo o se agote el timeout. Los valores por defecto (initial_wait de 10 s, timeout de 120 s) dan margen a la red, a menudo más lenta, de un runner de 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,
        })

Integrar el solver en pytest

Con pytest expones el solver y el navegador como fixtures. La del solver vive a nivel de sesión (una sola instancia para toda la suite) y la del navegador a nivel de función (un Chrome headless limpio en cada prueba):

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()

El archivo de pruebas

Cada test rellena el formulario, pide el token a CaptchaAI y lo inyecta en el campo oculto (g-recaptcha-response para reCAPTCHA v2, cf-turnstile-response para Turnstile) antes de enviar. Fíjate en que las URLs apuntan a un entorno de staging propio, nunca a producción:

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.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("ci@test.com")
        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"))
        )

Workflow de GitHub Actions

Instala Chrome y ChromeDriver, corre la suite y sube el reporte HTML como artefacto. La clave está en pasar la API key desde secrets, nunca en texto plano:

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

Configuración en GitLab CI

El mismo patrón en GitLab: un servicio de Selenium con Chrome y la suite como stage de test:

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

Pipeline de Jenkins

En Jenkins, credentials('captchaai-api-key') inyecta la API key como variable de entorno del pipeline, así el paso de tests la lee sin exponerla en los logs:

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'
        }
    }
}

Controlar el costo de resolver CAPTCHA en CI

Como los planes de CaptchaAI se facturan por thread (concurrencia) y no por resolución, el gasto depende de cuántas pruebas de CAPTCHA lanzas y con qué frecuencia. Para agencias y freelancers que facturan en monedas locales volátiles, un costo mensual fijo en USD es más fácil de presupuestar que el pago por resolución. Dos hábitos mantienen la factura bajo control.

Resuelve solo cuando haga falta

No todas las corridas necesitan resolver un CAPTCHA real. Salta esas pruebas en los builds de PR y déjalas para el merge a main o para la corrida nocturna:

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

Comprueba el saldo antes de lanzar la suite

Una suite que se queda sin saldo a mitad de camino falla de forma confusa. Comprueba el saldo al inicio y salta la suite con un mensaje claro si está por debajo del umbral:

@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}")

Errores frecuentes y cómo resolverlos

Problema Causa Solución
CAPTCHAAI_API_KEY not set El secreto no está configurado Añade la clave a los secretos de tu CI
Chrome falla en CI Falta el flag --no-sandbox Añade los flags de Chrome en modo headless
Pasa en local pero falla en CI Versión de navegador distinta Fija (pinea) la versión de Chrome en CI
El CAPTCHA agota el tiempo de espera La red del runner de CI es lenta Aumenta el parámetro timeout
La suite sale cara Demasiadas resoluciones por ejecución Usa SKIP_CAPTCHA_TESTS en los builds de PR

Preguntas frecuentes

¿Qué tipos de CAPTCHA puedo resolver en mis pruebas E2E?

Los más habituales en flujos de login y formularios: reCAPTCHA v2 y v3, Cloudflare Turnstile y Cloudflare Challenge, GeeTest v3 y los CAPTCHA de imagen/OCR. hCaptcha y FunCaptcha no son compatibles por ahora, así que no cuentes con ellos en la suite.

¿Cuánto cuesta resolver CAPTCHA dentro del pipeline?

Los planes se facturan por thread (ejecuciones simultáneas), no por resolución, y cada thread incluye resoluciones ilimitadas durante el mes. Para una suite que corre de noche, BASIC ($15/mes, 5 threads) suele sobrar; si paralelizas mucho con pytest-xdist, pasa a un plan con más threads.

¿La resolución ralentiza mucho el build?

Cada CAPTCHA añade unos segundos mientras el token viaja de ida y vuelta, algo despreciable frente al arranque del navegador y la carga de páginas. Si el runner está lejos de la API, sube initial_wait y timeout en lugar de recortar la cobertura.

¿Cómo evito que un CAPTCHA lento tumbe todo el job?

Deja que el timeout del helper acote cada resolución y marca esas pruebas para que puedan reintentarse. Así un pico puntual de latencia hace fallar una prueba aislada, no toda la corrida de CI.

Guías relacionadas

Deja de perseguir builds en rojo por un CAPTCHA: crea tu cuenta en CaptchaAI y guárdala como secreto de tu CI.

Los comentarios están deshabilitados para este artículo.