Tutoriales

Crea un pipeline de pruebas automatizadas con CaptchaAI

Cuando un formulario protegido por reCAPTCHA aparece en mitad de una prueba end-to-end, la suite se cuelga y el pipeline de CI falla. La salida no es desactivar el CAPTCHA en staging ni omitir el test: es resolverlo dentro de la propia prueba. En esta guía integras CaptchaAI como un fixture de pytest para que tus tests de Selenium obtengan un token válido, lo inyecten en el navegador y continúen.

Piensa en un equipo de QA que corre su suite E2E cada noche contra staging: cada login y cada checkout de prueba pasa por un reCAPTCHA v2. Con un solver por API, la ejecución no depende de nadie que resuelva el desafío a mano, y el costo mensual queda predecible en USD.

Esto es lo que vas a montar:

  • Un helper de CaptchaAI que resuelve el reCAPTCHA v2 y devuelve el token.
  • Fixtures de pytest que lanzan Selenium en modo headless.
  • Tests de login y de checkout que inyectan el token y validan el flujo.
  • Un workflow de GitHub Actions que lo ejecuta todo en CI.

Estructura de carpetas del proyecto de pruebas

La lógica de resolución vive en un helper aislado y los tests solo la consumen, así reutilizas el mismo CaptchaTestHelper en login, checkout y cualquier test futuro sin duplicar nada.

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

Cómo funciona la resolución paso a paso

El helper sigue el patrón de envío y sondeo de la API de CaptchaAI:

  1. Envías el sitekey y la URL de la página al endpoint in.php y recibes un ID de tarea.
  2. Consultas res.php cada pocos segundos hasta que el token esté listo.
  3. Inyectas el token en el campo g-recaptcha-response y disparas el callback de reCAPTCHA.

El helper que resuelve el CAPTCHA

Este es el núcleo. El helper lee la CAPTCHAAI_API_KEY del entorno, envía el sitekey y la URL al endpoint in.php y luego consulta res.php hasta recibir el token, reintentando mientras la respuesta sea CAPCHA_NOT_READY. El método inject_token es la pieza que más falla: no basta con escribir el token en g-recaptcha-response, también hay que disparar el callback de reCAPTCHA para que la página lo dé por válido.

En concreto, CaptchaTestHelper se encarga de:

  • Leer la CAPTCHAAI_API_KEY del entorno y validar que exista.
  • Enviar la tarea y sondear el resultado con reintentos.
  • Inyectar el token resuelto en el navegador de Selenium.
# 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)

Fixtures de pytest

Los fixtures conectan el helper con Selenium: captcha_solver con alcance de sesión y browser en modo headless por cada test, lo habitual en CI. El base_url apunta a tu staging; prueba siempre contra un dominio propio y autorizado.

# 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"

Test del flujo de login con CAPTCHA

El primer caso cubre el camino feliz y el de error. El patrón es idéntico: rellenas el formulario, lees el data-sitekey, pides el token, lo inyectas y envías. La diferencia está en la aserción final, para verificar que pruebas la lógica de autenticación y no solo la resolución del desafío.

# 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("test@example.com")
        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("wrong@example.com")
        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

Test del flujo de checkout

El checkout es el flujo crítico de cualquier tienda y suele llevar reCAPTCHA en la confirmación. Este test recorre el camino completo y espera con WebDriverWait el bloque de confirmación, en lugar de asumir que la página ya cargó.

# 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

Configuración de pytest

Cada resolución consume saldo, así que marca con captcha los tests que llaman al solver: en local corres el resto de la suite gratis y dejas los de CAPTCHA para la corrida en CI.

# tests/pytest.ini
[pytest]
markers =
    captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short

Workflow de GitHub Actions

Este workflow ejecuta la suite en cada push a main y también los lunes por la mañana. La CAPTCHAAI_API_KEY llega desde los secrets del repositorio y solo se corren los tests marcados con captcha para no gastar saldo de más.

# .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

Resolución de problemas frecuentes

Estos son los fallos más habituales al llevar el solver a CI y cómo resolverlos:

Problema Causa Solución
La inyección del token falla No se encuentra el textarea Verifica el ID del elemento o usa querySelector('[name="g-recaptcha-response"]')
Los tests pasan en local pero fallan en CI Versión distinta de Chrome Fija la versión de Chrome en la configuración de CI
El CAPTCHA no aparece en staging Staging desactiva el CAPTCHA Habilita el CAPTCHA en la configuración del entorno de staging
Tiempo de espera agotado esperando la solución Red lenta en CI Sube el tiempo de espera del sondeo a 180 s

Buenas prácticas para el CAPTCHA en CI

Unas cuantas reglas mantienen el pipeline estable y el gasto bajo control:

  • Guarda la CAPTCHAAI_API_KEY como secret del repositorio, nunca en el código.
  • Marca con captcha solo los tests que llaman al solver y córrelos aparte.
  • Prueba siempre contra un entorno de staging propio y autorizado.
  • Sube el tiempo de espera del sondeo si tu runner de CI tiene la red lenta.

Preguntas frecuentes

Las dudas que más se repiten al montar este pipeline por primera vez:

¿Qué plan de CaptchaAI necesito para una suite E2E?

Para una suite nocturna con pocos tests, el plan BASIC ($15/mes, 5 threads) suele bastar: cada thread resuelve un CAPTCHA a la vez y se libera al terminar. Si paralelizas muchos jobs de CI, sube a STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads).

¿El CAPTCHA hace más lentas mis pruebas?

Cada resolución añade unos segundos porque el helper espera y sondea el resultado. Aísla esos tests con el marcador captcha para no pagar ese tiempo en cada push local.

¿Puedo simular el CAPTCHA en las pruebas unitarias?

Sí. Haz un mock de CaptchaTestHelper.solve_recaptcha en los tests unitarios y reserva las resoluciones reales para las pruebas de integración E2E.

¿Sirve este enfoque con Cloudflare Turnstile además de reCAPTCHA v2?

Sí. La estructura es la misma: cambia el method y el campo del token (cf-turnstile-response para Turnstile). CaptchaAI resuelve reCAPTCHA v2/v3, Cloudflare Turnstile y GeeTest v3, entre otros tipos compatibles.

¿Necesito un proxy para resolver el CAPTCHA en las pruebas?

No para reCAPTCHA v2: el helper envía el sitekey y la URL de la página, y CaptchaAI resuelve el desafío por su cuenta. Reserva una salida de red autorizada para los escenarios que de verdad la exijan.


Guías relacionadas

Para profundizar en la resolución de CAPTCHA dentro de flujos de QA autorizados:


No dejes que el CAPTCHA bloquee tus pruebas. Empieza con CaptchaAI e integra el solver en tu pipeline de CI/CD.

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