Un CAPTCHA sur votre formulaire d'inscription bloque net votre suite de tests : Selenium remplit les champs, puis reste coincé devant le défi. La parade : déléguez la résolution à une API et le test poursuit son parcours jusqu'à la confirmation. Ce guide construit ce harnais en Python avec Selenium et l'API CaptchaAI (reCAPTCHA v2 et Cloudflare Turnstile).
Pourquoi un test d'inscription doit savoir gérer le CAPTCHA
Les formulaires d'inscription sont presque toujours protégés par un CAPTCHA, la première barrière contre la création automatisée de comptes. Sans résolution du défi, aucun de ces scénarios n'aboutit :
- Vérifier que le parcours d'inscription fonctionne en staging comme en production
- Couvrir les cas limites : e-mail déjà utilisé, mot de passe trop faible, champ obligatoire manquant
- Rejouer la suite de non-régression à chaque déploiement
- Confirmer que l'emplacement du CAPTCHA ne casse pas la mise en page ni la soumission
Le CAPTCHA n'est donc pas ce que vous testez : c'est l'obstacle qui vous sépare de la vraie logique métier. Le principe de ce guide est de le franchir proprement, une seule fois par parcours, puis de laisser le test valider ce qui compte — validation des champs, unicité de l'e-mail, règles de mot de passe.
Les CAPTCHA que vous croiserez sur un formulaire d'inscription
Avant d'écrire le moindre test, identifiez le widget en place : le champ dans lequel injecter le token en dépend. Trois protections couvrent la grande majorité des formulaires que vous rencontrerez.
| Protection | Où on la rencontre | Champ à renseigner |
|---|---|---|
| reCAPTCHA v2 | case « Je ne suis pas un robot », inscriptions grand public | g-recaptcha-response |
| Cloudflare Turnstile | sites derrière Cloudflare, SaaS B2B | cf-turnstile-response |
| reCAPTCHA v3 | score calculé en arrière-plan, sans clic | g-recaptcha-response |
Le parcours de test en un coup d'œil
Chaque cas suit la même trajectoire, quel que soit le scénario testé :
- Générer un utilisateur de test unique.
- Remplir le formulaire, cas limites compris.
- Résoudre le CAPTCHA via l'API et injecter le token.
- Soumettre, puis vérifier l'état du compte.
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
Construire le harnais de test
Générer des données d'inscription jetables
Chaque exécution a besoin d'un utilisateur unique, sinon le rejet « e-mail en double » se déclenche dès le deuxième passage. Un horodatage et quelques caractères aléatoires suffisent, sur un domaine d'exemple sans donnée personnelle réelle — la bonne pratique côté RGPD.
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,
}
Le testeur d'inscription de bout en bout
Cette classe orchestre tout le parcours. Concrètement, elle :
- détecte le type de CAPTCHA présent sur la page,
- envoie le sitekey à l'API et récupère le token,
- injecte le token dans le champ attendu (
cf-turnstile-responseoug-recaptcha-response), - remplit les champs, soumet, puis collecte erreurs et indicateurs de réussite.
La détection teste Turnstile en premier, puis retombe sur reCAPTCHA — l'ordre reflète simplement la façon dont ces widgets s'exposent dans le DOM.
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()
Lancer la suite
Instanciez le testeur avec votre clé API et l'URL de staging, puis laissez run_all() enchaîner les trois cas ; le try/finally ferme le navigateur même en cas d'exception.
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
Sortie :
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
Brancher la suite sur pytest
Pour l'intégrer à votre pipeline, exposez chaque cas comme un test pytest et partagez un même navigateur via une fixture de portée module — vous récupérez le reporting natif de pytest et l'exécution en CI.
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"
Étendre la couverture des cas de test
Les trois cas de départ — compte valide, e-mail en double, mot de passe faible — forment un socle. Une fois le CAPTCHA géré une bonne fois, ajouter des variantes ne coûte presque rien, car chacune réutilise le même harnais :
- champ obligatoire laissé vide (nom, e-mail, case de consentement RGPD),
- format d'e-mail invalide ou domaine jetable refusé,
- conditions d'utilisation non cochées avant l'envoi,
- mot de passe identique à l'e-mail ou trop court,
- caractères accentués et espaces dans le nom d'utilisateur.
Résoudre les problèmes courants
| Symptôme | Cause probable | Correctif |
|---|---|---|
| L'inscription réussit mais le test la déclare en échec | L'indicateur de réussite n'est pas reconnu | Ajoutez le texte de confirmation de votre site à _check_success() |
| Aucun CAPTCHA détecté | Le widget se charge après un délai | Insérez time.sleep(2) avant _solve_captcha() |
| Champs introuvables | Structure HTML différente de la vôtre | Adaptez les sélecteurs CSS à votre formulaire |
| Token refusé au moment d'envoyer | Résolu trop tôt, le token a expiré | Rapprochez _solve_captcha() de la soumission |
Bonnes pratiques pour des tests d'inscription fiables
- Résolvez le CAPTCHA juste avant la soumission pour limiter l'expiration du token.
- Stockez la clé API dans une variable d'environnement, jamais en dur dans le code.
- Préfixez les comptes de test (
test_) et purgez-les à la fin de la suite. - Restez sur le staging : n'automatisez que les environnements que vous êtes autorisé à tester.
Questions fréquentes
Quels types de CAPTCHA ce harnais prend-il en charge ?
reCAPTCHA v2 et Cloudflare Turnstile, les deux protections les plus courantes sur les formulaires d'inscription. CaptchaAI résout aussi reCAPTCHA v3 et GeeTest v3 ; hCaptcha, lui, n'est pas pris en charge.
Comment éviter que le token expire avant l'envoi ?
Appelez _solve_captcha() juste avant de cliquer sur le bouton de soumission, pas au début du remplissage. Plus le délai entre la résolution et l'envoi est court, plus le risque de rejet « token expiré » diminue.
Comment rester conforme au RGPD avec des comptes de test ?
N'utilisez que des données synthétiques, comme le fait TestUser avec un domaine d'exemple. Ne saisissez jamais d'e-mails ni d'identités réels dans un environnement de test, et supprimez les comptes créés une fois la suite terminée.
Puis-je exécuter cette suite dans un pipeline CI/CD ?
Oui. Passez Chrome en mode headless (options.add_argument("--headless")) et injectez la clé API via une variable d'environnement du runner plutôt qu'en dur dans le code.
Guides connexes
Testez vos parcours d'inscription sans rester bloqué par les CAPTCHA — utiliser CaptchaAI.