Tutorials

Construa um pipeline de testes automatizados com CaptchaAI

Um login protegido por reCAPTCHA v2 é o motivo mais comum de uma suíte E2E travar no CI. Desativar o CAPTCHA em staging testa um fluxo que não existe em produção; a alternativa é resolvê-lo de verdade, via API, dentro do teste. Este guia monta esse pipeline: helper de CAPTCHA, fixtures do pytest, testes de login e checkout com Selenium, e o workflow do GitHub Actions.

O mesmo helper serve para qualquer teste que dependa de reCAPTCHA v2 — não só login e checkout. Uma vez que a classe CaptchaTestHelper existe, basta importá-la em qualquer módulo de teste que precise atravessar o desafio.

Use sempre dados fictícios ([email protected], endereços de exemplo) e o domínio staging.example.com nos testes de CAPTCHA. Isso evita persistir dados reais de usuários em logs de CI — relevante inclusive para obrigações da LGPD ao lidar com dados de teste.


Pré-requisitos para o pipeline de testes

Antes de montar os arquivos abaixo, confirme que o ambiente tem:

  • Python 3.11+ com pytest, selenium e requests instalados.
  • Uma conta CaptchaAI ativa com chave de API válida, exportada como CAPTCHAAI_API_KEY.
  • Chrome instalado localmente e disponível no runner de CI (o workflow abaixo usa browser-actions/setup-chrome).
  • Um ambiente de staging com reCAPTCHA v2 ativo — este guia parte do princípio de que o CAPTCHA nunca é desligado para testes.

Estrutura do projeto de testes

Separe o helper de CAPTCHA das rotinas de navegador:

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

Helper de CAPTCHA para os testes

O helper concentra o envio e a consulta do token:

# 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 do pytest

Uma fixture instancia o helper; a outra sobe um Chrome headless:

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

Teste de login com CAPTCHA

O teste preenche o formulário, resolve e injeta o token, depois valida o redirecionamento:

# 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("[email protected]")
        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("[email protected]")
        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

Teste de checkout com CAPTCHA

No checkout o CAPTCHA costuma aparecer depois de carrinho e endereço preenchidos:

# 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

Configuração do pytest

Um marcador isola os testes que gastam créditos:

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

GitHub Actions: workflow de CI

Injete a chave da API como secret e rode os testes captcha no schedule semanal, não a cada commit:

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

Runner self-hospedado em São Paulo

Times que rodam o CI num runner self-hosted na região sa-east-1 (São Paulo) da AWS, em vez do ubuntu-latest hospedado pela GitHub, tendem a ver o solve_recaptcha retornar bem antes do timeout de 30 s configurado no helper — a proximidade geográfica com o endpoint da CaptchaAI reduz o RTT da requisição inicial ao in.php. Em runners hospedados fora da América do Sul, o mesmo timeout de 30 s costuma ser suficiente, mas vale monitorar o tempo total do passo Run E2E tests ao trocar de região.


Boas práticas para manter o pipeline de CI estável

Depois que os testes acima passam localmente, alguns ajustes evitam que o pipeline fique instável em produção:

  • Rode a suíte de CAPTCHA no schedule semanal, não a cada push — é assim que o workflow acima já está configurado, e evita gastar créditos em cada commit.
  • Isole os testes com o marcador captcha (pytest -m captcha) para que PRs pequenos rodem a suíte rápida sem esperar pela resolução do reCAPTCHA.
  • Monitore o saldo de créditos da CaptchaAI junto com o dashboard de CI — um saldo zerado derruba a suíte inteira com um erro genérico de submissão.
  • Aumente o timeout do polling (o loop em solve_recaptcha) antes de aumentar o timeout do teste inteiro — na maioria dos casos o gargalo é a rede do runner, não o Selenium.
  • Não pare de exibir o CAPTCHA em staging para "facilitar" o CI — isso testa um fluxo que a produção nunca executa.

Solução de problemas

Problema Causa Correção
Injeção do token falha Elemento textarea não encontrado Verifique o ID do elemento ou use querySelector('[name="g-recaptcha-response"]')
Testes passam localmente, falham no CI Versão diferente do Chrome Fixe a versão do Chrome na configuração do CI
CAPTCHA ausente em staging O ambiente de staging desativa o CAPTCHA Habilite o CAPTCHA na configuração do ambiente de teste
Timeout aguardando a resolução Rede lenta no CI Aumente o timeout do polling para 180 s
Testes de CAPTCHA falham em lote no CI Saldo de créditos da CaptchaAI zerado Verifique o saldo no painel antes de investigar o código do teste

Perguntas frequentes

Preciso desativar o CAPTCHA em staging para os testes passarem?

Não. Isso testaria um caminho que não existe em produção. Mantenha-o ativo e resolva-o de verdade, como neste guia.

Quanto custa rodar os testes de CAPTCHA todos os dias?

Poucos centavos por resolução. Uma suíte de 10 testes diários fica abaixo de US$ 10/mês.

Rodar os testes em paralelo no CI consome mais threads?

Sim, cada teste simultâneo ocupa uma thread até o token voltar. Com 10 jobs em paralelo, o BASIC (US$ 15/mês, 5 threads) pode enfileirar; o STANDARD (US$ 30/mês, 15 threads) resolve isso.

Como pulo os testes de CAPTCHA na minha máquina local?

Use pytest -m "not captcha", que ignora os testes marcados com @pytest.mark.captcha.

Esse pipeline funciona com outro tipo de CAPTCHA?

Sim — muda só o method enviado ao in.php. A CaptchaAI resolve Turnstile e GeeTest v3; hCaptcha/FunCaptcha ainda não.


Guias relacionados


Nunca deixe o CAPTCHA travar seus testes — comece com a CaptchaAI.

Os comentários estão desativados para este artigo.