Un formulaire de connexion protégé par reCAPTCHA v2 arrête net une suite de tests automatisés : le widget attend une interaction humaine que votre runner Pytest ne peut pas fournir. En envoyant le sitekey à l'API CaptchaAI, vous récupérez un token valide et votre test poursuit son parcours comme un vrai utilisateur, sans clic manuel. Ce guide montre comment brancher cette résolution sur Pytest, Selenium et vos pipelines CI/CD, tout en gardant les coûts maîtrisés.
Où les CAPTCHA bloquent vos tests QA
Dès qu'un widget reCAPTCHA v2 protège une action, il devient un point de rupture pour l'automatisation. Voici les campagnes de test où une résolution programmatique change tout :
| Scénario | Ce que la résolution automatique apporte |
|---|---|
| Tests de régression | Confirmer que les formulaires passent toujours après un déploiement |
| Tests de bout en bout | Rejouer un parcours utilisateur complet, connexion comprise |
| Tests de charge | Reproduire des flux CAPTCHA réalistes à grande échelle |
| Tests multi-navigateurs | Vérifier le rendu du widget sur Chrome, Firefox et Edge |
| Tests d'accessibilité | Valider les parcours alternatifs pour les utilisateurs en situation de handicap |
Le point commun de ces scénarios : le CAPTCHA n'est pas ce que vous testez, mais il se dresse entre votre script et l'étape qui vous intéresse vraiment.
Rester dans un périmètre autorisé
Ces techniques visent uniquement vos propres applications, sur un environnement de staging ou de recette que vous contrôlez, jamais un site tiers.
Avant de brancher la résolution à votre suite, cadrez le périmètre : automatiser un parcours protégé sur une application que vous n'exploitez pas sort du cadre de ce guide.
Réflexes RGPD à conserver
Pour les lecteurs soumis au RGPD, la conformité tient à quelques habitudes simples :
- Utilisez des comptes et des jeux de données de test, sans aucune donnée personnelle réelle.
- Hébergez votre staging au plus près de vos runners (OVHcloud, Scaleway ou une région AWS eu-west-3 à Paris pour limiter la latence).
- Journalisez le sitekey et l'issue du test, jamais les identifiants injectés dans le formulaire.
- Documentez qui a autorisé l'automatisation de ce parcours et sur quel environnement.
Intégrer la résolution de CAPTCHA à Pytest
Le helper ci-dessous encapsule tout le cycle de résolution derrière une seule méthode. Concrètement, il enchaîne trois étapes :
- Envoyer le sitekey et l'URL de la page à l'endpoint
in.phpde l'API. - Interroger
res.phpà intervalle régulier jusqu'à ce que le token soit prêt. - Renvoyer le token, qui part ensuite dans le champ
g-recaptcha-responsede la requête de connexion.
Une fixture de session partage la même clé API entre tous les tests et saute proprement la suite si CAPTCHAAI_API_KEY n'est pas défini, ce qui évite des échecs bruyants quand la variable manque en local.
Le helper et les cas de test de connexion
La classe TestLoginFlow couvre les trois cas qui comptent : identifiants valides avec token, identifiants invalides mais token présent, et soumission sans CAPTCHA que le serveur doit rejeter.
import pytest
import requests
import time
class CaptchaTestHelper:
"""Helper for solving CAPTCHAs in test environments."""
def __init__(self, api_key):
self.api_key = api_key
def solve_recaptcha(self, sitekey, pageurl, timeout=120):
"""Solve reCAPTCHA and return token."""
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()
assert result.get("status") == 1, f"Submit failed: {result}"
task_id = result["request"]
deadline = time.time() + timeout
time.sleep(10)
while time.time() < deadline:
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(f"Solve error: {data['request']}")
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout")
@pytest.fixture(scope="session")
def captcha_helper():
"""Provide CaptchaAI helper for test session."""
import os
api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not api_key:
pytest.skip("CAPTCHAAI_API_KEY not set")
return CaptchaTestHelper(api_key)
class TestLoginFlow:
"""Test login flow behind reCAPTCHA."""
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
LOGIN_URL = "https://staging.example.com/login"
def test_login_with_valid_credentials(self, captcha_helper):
"""Verify login succeeds with valid creds and solved CAPTCHA."""
token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
assert token and len(token) > 100
resp = requests.post(self.LOGIN_URL, data={
"username": "test_user",
"password": "test_pass",
"g-recaptcha-response": token,
})
assert resp.status_code == 200
assert "Welcome" in resp.text
def test_login_with_invalid_credentials(self, captcha_helper):
"""Verify login fails gracefully with bad creds but valid CAPTCHA."""
token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
resp = requests.post(self.LOGIN_URL, data={
"username": "wrong_user",
"password": "wrong_pass",
"g-recaptcha-response": token,
})
assert resp.status_code in (200, 401)
assert "Invalid" in resp.text or "error" in resp.text.lower()
def test_login_without_captcha_fails(self):
"""Verify login rejects submissions without CAPTCHA."""
resp = requests.post(self.LOGIN_URL, data={
"username": "test_user",
"password": "test_pass",
})
assert resp.status_code in (400, 403, 422)
Test E2E avec Selenium
Pour un parcours réellement rendu dans le navigateur, Selenium reproduit ce que ferait un utilisateur, résolution du CAPTCHA comprise.
Extraire le sitekey et injecter le token
Le test enchaîne les mêmes gestes à chaque exécution :
- remplir le formulaire d'inscription et lire le
data-sitekeydu widget ; - résoudre le CAPTCHA via le helper, puis écrire le token dans le champ caché
g-recaptcha-response; - déclencher le callback
data-callbacks'il existe, avant de soumettre.
import pytest
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
@pytest.fixture
def browser():
"""Create browser for testing."""
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
class TestRegistrationFlow:
"""Test registration form with CAPTCHA."""
REG_URL = "https://staging.example.com/register"
def test_registration_form_submits(self, browser, captcha_helper):
"""Full registration flow with CAPTCHA solving."""
browser.get(self.REG_URL)
# Fill form
browser.find_element(By.ID, "email").send_keys("test@example.com")
browser.find_element(By.ID, "password").send_keys("SecurePass123!")
browser.find_element(By.ID, "confirm_password").send_keys("SecurePass123!")
# Extract sitekey from page
captcha_div = browser.find_element(By.CSS_SELECTOR, ".g-recaptcha")
sitekey = captcha_div.get_attribute("data-sitekey")
# Solve via API
token = captcha_helper.solve_recaptcha(sitekey, browser.current_url)
# Inject token
browser.execute_script("""
document.querySelector('[name="g-recaptcha-response"]').value = arguments[0];
""", token)
# Trigger callback if needed
callback = captcha_div.get_attribute("data-callback")
if callback:
browser.execute_script(f"window['{callback}'](arguments[0]);", token)
# Submit
browser.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# Verify success
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
)
def test_captcha_renders_on_page(self, browser):
"""Verify CAPTCHA widget loads on registration page."""
browser.get(self.REG_URL)
captcha = WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, iframe[src*='recaptcha']"))
)
assert captcha.is_displayed()
Isoler et configurer les tests CAPTCHA
Placez les tests qui consomment des résolutions derrière un marqueur Pytest dédié. Vous décidez ainsi de les exécuter ou non selon le pipeline, sans toucher au reste de la suite.
Déclarer le marqueur captcha
# conftest.py
import os
# Mark tests that need CAPTCHA solving
def pytest_configure(config):
config.addinivalue_line(
"markers", "captcha: tests requiring CAPTCHA solving (may be slow)"
)
# pytest.ini or pyproject.toml
"""
[tool.pytest.ini_options]
markers = [
"captcha: tests requiring CAPTCHA solving (may be slow)",
]
"""
N'exécuter que les tests CAPTCHA :
pytest -m captcha -v
Lancer le pipeline rapide, sans résolution :
pytest -m "not captcha" -v
Maîtriser le coût des tests CAPTCHA
Comme la facturation CaptchaAI repose sur les threads simultanés et non sur le nombre de résolutions, l'essentiel est de limiter la fréquence et le parallélisme des tests qui appellent l'API.
Cinq réflexes pour limiter la facture
| Stratégie | Bénéfice |
|---|---|
| Cibler un environnement de staging | Défis CAPTCHA généralement moins agressifs |
| Planifier les tests CAPTCHA plutôt qu'à chaque push | Moins d'appels à l'API |
| Mettre en cache les résultats des tests instables | Éviter les résolutions redondantes |
| Ignorer les CAPTCHA en local via un indicateur d'environnement | Développer sans coût de résolution |
| Regrouper les tests CAPTCHA dans un job CI dédié | Coûts prévisibles par pipeline |
En pratique, un plan à 5 threads absorbe une suite QA typique sans jamais atteindre son plafond, puisque les tests résolvent les CAPTCHA les uns après les autres plutôt qu'en rafale.
FAQ
Quels types de CAPTCHA CaptchaAI résout-il pour mes tests ?
Les types en accès général : reCAPTCHA v2 (y compris invisible et Enterprise), reCAPTCHA v3, Cloudflare Turnstile et Challenge, GeeTest v3, ainsi que les CAPTCHA image/OCR et en grille. hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge.
Le prix dépend-il du nombre de CAPTCHA résolus ?
Non. CaptchaAI facture des threads simultanés, pas les résolutions à l'unité, et chaque plan inclut des résolutions illimitées par thread. Le plan BASIC ($15/mois, 5 threads) suffit largement à une suite de tests, qui exécute rarement des résolutions massivement en parallèle.
Comment empêcher les tests CAPTCHA de ralentir la CI ?
Marquez chaque test qui résout un CAPTCHA avec le marqueur captcha, puis excluez-les des exécutions rapides via pytest -m "not captcha". Réservez ces tests à un job CI planifié plutôt qu'à chaque push, et laissez le reste de la suite tourner à pleine vitesse.
Puis-je simuler les CAPTCHA au lieu de les résoudre ?
Oui, et c'est recommandé pour les tests unitaires : mockez la réponse du CAPTCHA plutôt que d'appeler l'API. Réservez la résolution réelle aux tests d'intégration et E2E exécutés sur un environnement de staging autorisé.
Guides connexes
- Les bonnes pratiques éthiques de résolution de CAPTCHA
- Tester les CAPTCHA en CI/CD avec GitHub Actions
Automatisez votre pipeline d'assurance qualité et ajoutez CaptchaAI à vos tests.