Um formulário de cadastro protegido por CAPTCHA barra qualquer suíte de teste que não resolve o desafio antes de enviar os dados.
Este guia mostra como montar, em Python, uma suíte que preenche o formulário, resolve reCAPTCHA v2 ou Cloudflare Turnstile pela API da CaptchaAI e cobre os três cenários que toda equipe de QA precisa validar: cadastro válido, e-mail duplicado e senha fraca.
Rode a suíte só contra ambientes que você tem permissão para testar — staging, homologação ou uma instância própria. Nunca aponte esse código para o cadastro de terceiros.
Por que o CAPTCHA é o gargalo dos testes de cadastro
Quase todo formulário de registro tem CAPTCHA para barrar a criação automática de contas, e é exatamente esse widget que trava a automação de QA se ninguém resolver o desafio antes do envio. Sem cobertura para ele, a equipe fica sem regressão automatizada justamente na tela mais crítica do produto. É ali que o teste precisa confirmar que o cadastro funciona em staging e em produção, e que casos extremos como e-mail duplicado, senha fraca e campos obrigatórios em branco são tratados corretamente. A regressão também precisa rodar a cada deploy sem intervenção manual, sem que o posicionamento do widget quebre o layout ou o envio do formulário.
Isso muda a forma como a suíte precisa ser desenhada. Em vez de mockar o CAPTCHA ou pular a etapa em ambiente de teste — o que deixa a tela real sem cobertura —, a suíte abaixo resolve o desafio de verdade a cada execução, do mesmo jeito que um usuário real resolveria.
Como a suíte de teste está organizada
O fluxo tem quatro etapas: gerar dados de teste, preencher o formulário (incluindo os casos extremos), resolver o CAPTCHA e verificar se a conta foi criada.
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
Montando a suíte de teste em Python
Gerando dados de teste únicos
Cada execução precisa de um usuário novo — reaproveitar e-mail ou username entre rodadas derruba o teste de cadastro válido por colisão, não por bug real. A classe abaixo gera dados fictícios com timestamp e sufixo aleatório, sempre em um domínio de teste:
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,
}
A classe que resolve o CAPTCHA e roda os testes
CaptchaSolver envia o desafio para a API da CaptchaAI (in.php) e faz o polling do resultado em res.php até o token sair ou o tempo limite estourar. RegistrationTester usa esse token para preencher g-recaptcha-response ou cf-turnstile-response via Selenium antes de clicar em enviar:
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()
Como rodar a suíte
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
Saída:
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
Integrando com pytest
Para rodar como parte da suíte de CI, transforme o tester em uma fixture de módulo e cada cenário em um teste separado — assim o relatório do pytest mostra exatamente qual caso falhou, em vez de um bloco único de resultados:
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"
Dados de teste e LGPD
Como TestUser gera nome, e-mail e senha fictícios a cada execução, a suíte nunca reaproveita dados reais de clientes — o que também simplifica a conformidade com a LGPD. Dados sintéticos com prefixo test_ e domínio testmail.example.com não são dados pessoais de verdade, então não entram na mesma discussão de retenção e descarte que os dados reais de cadastro exigem.
Vale considerar também onde a suíte roda: se o CI está hospedado fora do Brasil mas o produto atende usuários brasileiros, meça a latência do solve a partir da região mais próxima da sua base de usuários. Workers em sa-east-1 (São Paulo), por exemplo, tendem a reduzir o tempo total do teste em relação a regiões nos EUA ou na Europa.
Problemas comuns e como resolver
- O cadastro deu certo, mas o teste aponta falha. O indicador de sucesso não bate com o texto real do seu site — adicione o texto de sucesso do seu produto em
_check_success(). - O CAPTCHA não é detectado. O widget carrega com atraso após o resto da página — insira
time.sleep(2)antes de chamar_solve_captcha(). - Campos do formulário não são encontrados. A estrutura HTML do seu site difere da esperada — ajuste os seletores CSS para o formulário do seu produto.
- O token expira antes do envio. O CAPTCHA foi resolvido cedo demais em relação ao clique em enviar — chame
_solve_captcha()o mais próximo possível do envio do formulário.
Perguntas frequentes
Antes de colocar essa suíte no pipeline, essas são as dúvidas que mais aparecem em equipes de QA.
Dá para rodar essa suíte em modo headless no CI?
Sim, e é o modo mais comum em pipeline. Use options.add_argument("--headless") no Chrome e guarde a chave de API da CaptchaAI como variável de ambiente do CI, nunca hardcoded no repositório.
Como evito reaproveitar dados reais de usuários nos testes?
Deixe o TestUser gerar tudo: nome, e-mail e senha ficam únicos a cada execução, sempre em testmail.example.com. Isso evita colisão entre rodadas e mantém a suíte longe de qualquer dado pessoal real.
Como faço a limpeza das contas de teste depois que a suíte roda?
Adicione um passo de teardown que exclua as contas via API administrativa do seu produto, ou padronize o prefixo test_ no username para identificar e apagar em lote depois.
A CaptchaAI resolve outros tipos de CAPTCHA além de reCAPTCHA v2 e Turnstile?
Sim — reCAPTCHA v2/v3, Cloudflare Turnstile e GeeTest v3 são suportados nativamente, e dá para estender _solve_captcha() para outros tipos suportados conforme o formulário de cadastro exigir. hCaptcha e FunCaptcha ainda não são suportados.
O que fazer se o teste falhar de forma intermitente, sem mudança no código?
Quase sempre é timing: aumente o initial_wait do _solve ou o time.sleep(3) após o clique em enviar. Formulários com validação assíncrona de e-mail costumam precisar de uma espera maior antes de checar o resultado.
Leitura complementar
Para expandir a cobertura de CAPTCHA para além do cadastro:
Teste fluxos de cadastro sem travar na tela de CAPTCHA — conheça a CaptchaAI.