Use Cases

Manipulação de CAPTCHA em testes de integração contínua

O pipeline de CI trava no CAPTCHA porque ele existe para bloquear exatamente esse tipo de execução automática — e é isso que um teste ponta a ponta faz o dia inteiro, sem ninguém na frente da tela. A saída não é remover o CAPTCHA do ambiente de staging nem simular o clique manualmente: é resolver o desafio via API dentro do próprio pipeline, com a chave guardada como secret e o resultado injetado direto no formulário antes do submit.


Por que o CI trava sempre que encontra um CAPTCHA

Pipelines de CI/CD rodam sozinhos, disparados por um git push ou por um agendamento — não há ninguém na frente do navegador para clicar em "não sou um robô" ou arrastar o slider do Turnstile. O CAPTCHA foi desenhado exatamente para exigir esse tipo de interação humana, então qualquer teste E2E que passe por uma tela de login, cadastro ou contato protegida por reCAPTCHA v2 ou Cloudflare Turnstile trava ali mesmo, sempre no mesmo ponto — não é um bug intermitente no seu código, é o comportamento esperado de um CAPTCHA funcionando.

A correção: plugar a API da CaptchaAI no conjunto de testes em vez de tentar remover o CAPTCHA do ambiente de teste. A chave de API fica armazenada como secret na plataforma de CI, e cada teste resolve o CAPTCHA automaticamente antes de submeter o formulário — o pipeline continua rodando sem intervenção humana em nenhum ponto do processo.


Como a arquitetura se encaixa no pipeline

O fluxo é simples de desenhar: o git push dispara o runner de CI, que sobe um Chrome headless e roda a suíte E2E; sempre que um teste encontra um CAPTCHA, ele chama a API da CaptchaAI em vez de travar, recebe o token de volta e segue em frente até gerar o relatório final. Nenhuma etapa depende de alguém olhar a tela.

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

Classe auxiliar para resolver CAPTCHA nos testes

O ponto de partida é uma classe fina em volta da API in.php / res.php, reutilizável em qualquer teste que precise de um token de reCAPTCHA v2 ou Cloudflare Turnstile:

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

Integrando com o pytest

Fixtures em conftest.py. Ficam disponíveis para todo arquivo de teste da suíte, sem precisar reimportar nada:

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

Casos de teste que passam por CAPTCHA. Login e formulário de contato só precisam pedir o token antes de submeter — o resto do fluxo é Selenium comum:

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

Exemplo prático: suíte de e-commerce rodando em São Paulo

Um cenário comum em times de QA no Brasil: uma loja virtual com checkout protegido por Cloudflare Turnstile e cadastro de conta protegido por reCAPTCHA v2, com runners self-hosted na região sa-east-1 da AWS para reduzir a latência até os servidores da aplicação. A cada merge na branch principal, a suíte E2E sobe um Chrome headless, passa por login, cadastro e checkout com dados fictícios de staging (nunca dados reais de cliente, o que também mantém o pipeline alinhado com a LGPD) e resolve os dois tipos de desafio via API antes de cada submit.

Com pytest-xdist rodando 8 a 10 testes em paralelo, o plano BASIC (US$ 15/mês, 5 threads) já fica apertado; a maioria dos times nesse cenário sobe direto para STANDARD (US$ 30/mês, 15 threads), que dá folga para picos de execução sem fila. O relatório HTML final sobe como artefato do próprio job — nenhum passo do fluxo exige alguém clicando em nada.


Configurando o GitHub Actions

Três blocos fazem o trabalho: instalar o Chrome e o ChromeDriver, rodar o pytest com a chave da CaptchaAI disponível como variável de ambiente vinda de um secret do repositório, e publicar o relatório HTML como artefato — mesmo quando algum teste falha.

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

Configurando o GitLab CI

A diferença aqui é o serviço selenium/standalone-chrome, que sobe como container ao lado do job em vez de instalar o Chrome direto no runner — o restante da lógica (chave da CaptchaAI como variável de CI, pytest rodando a suíte) é igual ao GitHub Actions.

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 no Jenkins

Em times que já usam Jenkins, a chave da CaptchaAI entra como credential gerenciada pelo próprio Jenkins (credentials('captchaai-api-key')), nunca hardcoded no Jenkinsfile — o restante do pipeline segue o mesmo padrão: instalar dependências, rodar a suíte e publicar o relatório JUnit.

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

Controlando o custo de CAPTCHA no CI

CAPTCHA resolvido via API tem custo por thread ativa, não por execução — mas rodar o conjunto completo em cada pull request ainda soma.

Resolva só quando for necessário. Reserve os testes com CAPTCHA para o merge na branch principal ou uma execução noturna:

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

Verifique o saldo antes de rodar o conjunto de testes. Uma fixture autouse barra a suíte antes de gastar créditos com saldo baixo:

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

Solução de problemas comuns

A maioria dos problemas de CAPTCHA em CI cai em três baldes: configuração de secret, ambiente do Chrome divergindo do local, ou rede lenta estourando o timeout. A tabela abaixo cobre os cinco casos mais comuns na prática:

Problema Causa Correção
CAPTCHAAI_API_KEY not set Secret não configurado Adicionar a chave aos secrets do CI
Chrome trava no runner Faltam --no-sandbox e --disable-dev-shm-usage Usar os flags completos de modo headless
Passa local, falha no CI Versão do Chrome diverge entre ambientes Fixar a versão do Chrome no runner
CAPTCHA expira antes de resolver Rede do runner lenta ou sobrecarregada Aumentar o timeout do solver
Conta esgota rápido Todo PR dispara o conjunto completo Usar SKIP_CAPTCHA_TESTS em builds de PR

Perguntas frequentes

Preciso resolver CAPTCHA em toda execução de CI?

Não. Rode esses testes na mesclagem com a branch principal ou em um agendamento noturno, e pule cada pull request com o flag SKIP_CAPTCHA_TESTS.

Como evito estourar o orçamento de CAPTCHA nos testes?

Combine SKIP_CAPTCHA_TESTS em builds de PR com uma verificação de saldo autouse no início da suíte, que interrompe com pytest.skip antes de gastar créditos.

Dá para rodar os testes de CAPTCHA em paralelo?

Sim. Cada teste pede seu próprio token e a CaptchaAI atende requisições simultâneas — use pytest-xdist para reduzir o tempo total do pipeline.

Uso dados reais de usuário nos testes de CI?

Não. Use apenas dados fictícios em staging, como testuser e [email protected] nos exemplos acima — isso mantém o pipeline alinhado com a LGPD e evita qualquer risco de vazar dado de cliente em log de build.

Quantas threads eu preciso para rodar a suíte em paralelo?

Depende de quantos testes com CAPTCHA rodam ao mesmo tempo. Cada teste em execução ocupa uma thread até o token voltar; com pytest-xdist disparando 5 a 10 testes simultâneos, o plano BASIC (US$ 15/mês, 5 threads) já enfileira, então STANDARD (US$ 30/mês, 15 threads) costuma ser o ponto de partida mais confortável para suítes de CI.


Guias relacionados


Adicione resolução de CAPTCHA ao seu CI — comece com a CaptchaAI.

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