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:
- Envías el
sitekeyy la URL de la página al endpointin.phpy recibes un ID de tarea. - Consultas
res.phpcada pocos segundos hasta que el token esté listo. - Inyectas el token en el campo
g-recaptcha-responsey 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_KEYdel 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_KEYcomo secret del repositorio, nunca en el código. - Marca con
captchasolo 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.