Casos de Uso

Manejo de CAPTCHA en las pruebas de flujo de registro

¿Tu suite de pruebas de registro se detiene en cuanto aparece el CAPTCHA? La salida no es desactivarlo ni pedirle a alguien que haga clic a mano: es resolver el desafío dentro del propio test. Tu automatización obtiene un token válido, lo inyecta en el formulario y sigue hasta comprobar que la cuenta se creó.

En esta guía montamos un probador de registro en Python con Selenium que detecta reCAPTCHA v2 y Cloudflare Turnstile, los resuelve con la API de CaptchaAI y valida los casos límite del alta sin intervención manual.


Por qué las pruebas de registro tropiezan con el CAPTCHA

El formulario de registro es casi siempre la puerta más protegida de una aplicación: lleva CAPTCHA para frenar el alta masiva de cuentas de bot. El problema es que ese mismo CAPTCHA bloquea a tu automatización de QA, que también es "un bot" a ojos del anti-bot. Sin una forma de resolverlo, cada prueba end-to-end se queda a medias y el equipo valida el registro a mano en cada despliegue.

Automatizar el flujo completo te permite:

  • Verificar que el alta funciona igual en staging y en producción.
  • Cubrir casos límite: correo duplicado, contraseña débil, campos obligatorios vacíos.
  • Ejecutar pruebas de regresión en cada despliegue, no solo antes de una release grande.
  • Confirmar que la posición del widget CAPTCHA no rompe el layout ni el envío del formulario.

En mercados de habla hispana esto es especialmente relevante: los portales públicos —cita previa en España, trámites del SAT en México— y los marketplace regionales protegen sus altas con CAPTCHA, y las agencias que integran sobre ellos necesitan probar ese registro de forma repetible.

Automatiza solo altas propias o con permiso, y respeta los términos del servicio y la normativa de protección de datos (GDPR y LOPDGDD en España, LFPDPPP en México).


Qué necesitas antes de empezar

Este método encaja cuando el formulario usa un tipo de CAPTCHA compatible:

  • Compatibles: reCAPTCHA v2 y v3, Cloudflare Turnstile y GeeTest v3, entre otros.
  • No compatibles hoy: hCaptcha y FunCaptcha. Si tu alta usa uno de ellos, este enfoque no aplica.

Comprueba qué widget monta tu formulario mirando el atributo data-sitekey o la clase del contenedor (.cf-turnstile para Turnstile). Con eso claro, reúne lo mínimo:

  • Python con Selenium y Chrome con su driver.
  • Tu YOUR_API_KEY de CaptchaAI en una variable de entorno.

El patrón es siempre el mismo: envías el sitekey y la URL de la página a la API, sondeas el resultado y escribes el token en el campo oculto que el formulario espera (g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile).


Arquitectura del probador de registro

El flujo tiene cuatro etapas encadenadas:

  1. Generas datos de usuario únicos.
  2. Rellenas el formulario cubriendo los casos límite.
  3. Resuelves el CAPTCHA.
  4. Verificas el estado de la cuenta.
┌──────────┐     ┌───────────────┐     ┌────────────┐     ┌────────────┐
│ Test Data │────▶│ Fill Form +   │────▶│ Solve      │────▶│ Verify     │
│ Generator │     │ Edge Cases    │     │ CAPTCHA    │     │ Account    │
└──────────┘     └───────────────┘     └────────────┘     └────────────┘

Implementación paso a paso

Generar datos de usuario únicos

Cada ejecución necesita un usuario nuevo para no chocar con altas anteriores. Este generador crea nombre, correo y contraseña únicos a partir de una marca de tiempo, así lanzas la suite cuantas veces quieras sin colisiones de correo duplicado (salvo cuando lo provocas a propósito).

import random
import string
import time


class TestUser:
    def __init__(self, prefix="test"):
        ts = int(time.time())
        rand = ''.join(random.choices(string.ascii_lowercase, k=4))
        self.first_name = f"{prefix}_{rand}"
        self.last_name = "User"
        self.email = f"{prefix}_{ts}_{rand}@testmail.example.com"
        self.username = f"{prefix}_{ts}_{rand}"
        self.password = f"Test!{ts}{rand.upper()}"

    def as_dict(self):
        return {
            "first_name": self.first_name,
            "last_name": self.last_name,
            "email": self.email,
            "username": self.username,
            "password": self.password,
        }

El probador de registro con resolución de CAPTCHA

Aquí está el núcleo. La clase CaptchaSolver habla con la API (in.php para enviar la tarea, res.php para sondear el resultado) y RegistrationTester orquesta cada caso de prueba: rellena campos, detecta si hay Turnstile o reCAPTCHA en la página, inyecta el token y comprueba si el alta tuvo éxito.

import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class CaptchaSolver:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    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,
        })

    def _solve(self, params, initial_wait=10):
        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(resp["request"])
        task_id = resp["request"]
        time.sleep(initial_wait)
        for _ in range(60):
            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(result["request"])
        raise TimeoutError("Timed out")


class RegistrationTester:
    def __init__(self, api_key, base_url):
        self.solver = CaptchaSolver(api_key)
        self.base_url = base_url
        self.driver = webdriver.Chrome()
        self.wait = WebDriverWait(self.driver, 10)
        self.results = []

    def _fill(self, selector, value):
        el = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
        el.clear()
        el.send_keys(value)

    def _solve_captcha(self):
        html = self.driver.page_source
        page_url = self.driver.current_url

        # Turnstile
        turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
        if turnstile:
            sitekey = turnstile[0].get_attribute("data-sitekey")
            token = self.solver.solve_turnstile(sitekey, page_url)
            self.driver.execute_script(
                f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
            )
            return

        # reCAPTCHA
        recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
        if recaptcha and "recaptcha" in html.lower():
            sitekey = recaptcha[0].get_attribute("data-sitekey")
            token = self.solver.solve_recaptcha(sitekey, page_url)
            self.driver.execute_script(
                f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
            )

    def _get_errors(self):
        """Collect any visible error messages on the page."""
        error_selectors = [
            ".error", ".alert-danger", ".form-error",
            "[role='alert']", ".validation-error",
        ]
        errors = []
        for sel in error_selectors:
            for el in self.driver.find_elements(By.CSS_SELECTOR, sel):
                text = el.text.strip()
                if text:
                    errors.append(text)
        return errors

    def _check_success(self):
        """Check if registration succeeded."""
        html = self.driver.page_source.lower()
        url = self.driver.current_url.lower()
        success_indicators = [
            "welcome", "account created", "verify your email",
            "registration successful", "thank you for registering",
        ]
        return any(ind in html or ind in url for ind in success_indicators)

    # --- Test Cases ---

    def test_valid_registration(self):
        """Test: Valid registration should succeed."""
        user = TestUser()
        self.driver.get(f"{self.base_url}/register")

        self._fill("[name='firstName'], #first-name", user.first_name)
        self._fill("[name='lastName'], #last-name", user.last_name)
        self._fill("[name='email'], #email", user.email)
        self._fill("[name='username'], #username", user.username)
        self._fill("[name='password'], #password", user.password)

        confirm_fields = self.driver.find_elements(By.CSS_SELECTOR, "[name='confirmPassword'], #confirm-password")
        if confirm_fields:
            confirm_fields[0].clear()
            confirm_fields[0].send_keys(user.password)

        self._solve_captcha()

        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        time.sleep(3)

        success = self._check_success()
        self.results.append({
            "test": "valid_registration",
            "passed": success,
            "user": user.email,
            "errors": self._get_errors() if not success else [],
        })
        return success

    def test_duplicate_email(self):
        """Test: Duplicate email should show error."""
        user = TestUser()

        # First registration
        self.driver.get(f"{self.base_url}/register")
        self._fill("[name='email'], #email", user.email)
        self._fill("[name='password'], #password", user.password)
        self._fill("[name='firstName'], #first-name", user.first_name)
        self._solve_captcha()
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        time.sleep(3)

        # Second registration with same email
        self.driver.get(f"{self.base_url}/register")
        self._fill("[name='email'], #email", user.email)
        self._fill("[name='password'], #password", user.password)
        self._fill("[name='firstName'], #first-name", "Duplicate")
        self._solve_captcha()
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        time.sleep(3)

        errors = self._get_errors()
        has_error = len(errors) > 0 or not self._check_success()
        self.results.append({
            "test": "duplicate_email",
            "passed": has_error,
            "errors": errors,
        })
        return has_error

    def test_weak_password(self):
        """Test: Weak password should be rejected."""
        user = TestUser()
        self.driver.get(f"{self.base_url}/register")

        self._fill("[name='email'], #email", user.email)
        self._fill("[name='password'], #password", "123")  # Weak password
        self._fill("[name='firstName'], #first-name", user.first_name)
        self._solve_captcha()
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        time.sleep(3)

        errors = self._get_errors()
        rejected = len(errors) > 0 or not self._check_success()
        self.results.append({
            "test": "weak_password",
            "passed": rejected,
            "errors": errors,
        })
        return rejected

    def run_all(self):
        """Run all registration tests."""
        tests = [
            self.test_valid_registration,
            self.test_duplicate_email,
            self.test_weak_password,
        ]
        for test_fn in tests:
            try:
                test_fn()
            except Exception as e:
                self.results.append({
                    "test": test_fn.__name__,
                    "passed": False,
                    "errors": [str(e)],
                })
        return self.results

    def report(self):
        passed = sum(1 for r in self.results if r["passed"])
        total = len(self.results)
        lines = [f"Registration Tests: {passed}/{total} passed", "-" * 40]
        for r in self.results:
            status = "PASS" if r["passed"] else "FAIL"
            lines.append(f"  [{status}] {r['test']}")
            if r.get("errors"):
                for err in r["errors"]:
                    lines.append(f"         {err}")
        return "\n".join(lines)

    def close(self):
        self.driver.quit()

Ejecutar la suite

Instancia el probador contra tu entorno de staging, ejecuta todos los casos y cierra el navegador en el bloque finally para no dejar sesiones colgadas si algo falla.

tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")

try:
    tester.run_all()
    print(tester.report())
finally:
    tester.close()

Salida:

Registration Tests: 3/3 passed
----------------------------------------
  [PASS] valid_registration
  [PASS] duplicate_email
  [PASS] weak_password

Integrar con pytest

Para que esto viva en tu pipeline de CI, envuelve el probador en un fixture de pytest. Cada caso se convierte en un test independiente con su propio aserto, así el reporte de CI te dice exactamente qué escenario del alta se rompió.

import pytest


@pytest.fixture(scope="module")
def tester():
    t = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
    yield t
    t.close()


def test_valid_registration(tester):
    assert tester.test_valid_registration(), "Valid registration should succeed"


def test_duplicate_email_rejected(tester):
    assert tester.test_duplicate_email(), "Duplicate email should be rejected"


def test_weak_password_rejected(tester):
    assert tester.test_weak_password(), "Weak password should be rejected"

Errores comunes y cómo resolverlos

La mayoría de los fallos no vienen de la resolución del CAPTCHA, sino de selectores que no coinciden con tu sitio o de tiempos mal ajustados. Esta tabla cubre los cuatro que más aparecen:

Problema Causa Solución
El alta funciona pero el test la marca como fallida El indicador de éxito no coincide Añade el texto de éxito de tu sitio a _check_success()
No se detecta el CAPTCHA El widget carga con retraso Añade time.sleep(2) antes de _solve_captcha()
No se encuentran los campos La estructura HTML es distinta Actualiza los selectores CSS para tu sitio
Token caducado Se resolvió demasiado pronto Llama a _solve_captcha() justo antes de enviar el formulario

Un detalle sobre el último punto: el token de reCAPTCHA v2 caduca a los pocos minutos, así que conviene resolver el CAPTCHA como último paso antes del clic en enviar.


Cuánto cuesta resolver CAPTCHA en tu CI

CaptchaAI factura por threads concurrentes, no por resolución, y cada plan incluye resoluciones ilimitadas por thread durante el mes.

Para una suite de CI esto encaja bien: como los tests de registro suelen correr en secuencia, un solo thread ya te sirve.

  • BASIC ($15/mes, 5 threads) cubre de sobra una suite de CI que corre en serie.
  • STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) si paralelizas los tests en muchos jobs simultáneos.

Ese costo mensual fijo en USD, sin cargo por resolución, es predecible para agencias y freelancers que facturan en monedas locales volátiles: sabes exactamente lo que gastas en QA cada mes.


Preguntas frecuentes

¿Qué tipos de CAPTCHA puedo resolver en el formulario de registro?

reCAPTCHA v2 y v3, Cloudflare Turnstile y GeeTest v3, entre otros. El ejemplo detecta automáticamente Turnstile (.cf-turnstile) y reCAPTCHA ([data-sitekey]). hCaptcha y FunCaptcha no están soportados; si tu alta usa uno de esos, este método no aplica.

¿La resolución del CAPTCHA ralentiza mucho mi suite?

Depende del tipo. Turnstile y reCAPTCHA v2 suelen resolverse en pocos segundos, y el código sondea el resultado en lugar de bloquear con una espera fija larga. Frente a validar el alta a mano en cada despliegue, la suite automatizada sigue siendo mucho más rápida.

¿Necesito un plan grande para ejecutar pruebas en CI?

No. Como los tests de registro corren en secuencia, un thread basta y el plan BASIC ($15/mes, 5 threads) es suficiente. Sube a STANDARD o ADVANCE solo si ejecutas muchos jobs de CI en paralelo.

¿Es correcto automatizar el registro con CAPTCHA?

Sí, sobre tus propias aplicaciones o sobre sitios donde tengas permiso explícito para probar. Esta guía es para QA de tu propio flujo de alta; respeta los términos del servicio y la normativa de protección de datos (GDPR, LOPDGDD, LFPDPPP) al generar y limpiar cuentas de prueba.


Guías relacionadas


Prueba tus flujos de registro sin que el CAPTCHA los bloquee. Empieza con CaptchaAI.

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