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.