Un CAPTCHA sur une page de connexion ou de paiement suffit à faire échouer toute une exécution de tests de bout en bout. La parade : votre pipeline de tests automatisés appelle l'API CaptchaAI, récupère un token reCAPTCHA v2 valide, l'injecte dans le navigateur Selenium, puis laisse le scénario se poursuivre.
Ce guide monte ce pipeline avec pytest, Selenium et GitHub Actions. Tout vise des environnements que vous contrôlez — staging ou préproduction — avec des données de test synthétiques : côté RGPD, ne rejouez jamais d'identifiants ni d'informations personnelles réelles. Le principe reste le même pour Cloudflare Turnstile ou GeeTest v3 : seuls la méthode d'API et le champ de token changent.
Périmètre sûr : ces scénarios s'exécutent sur vos propres environnements de staging, avec des comptes et des données de test. N'automatisez jamais le service d'un tiers sans autorisation explicite.
Concrètement, le pipeline couvre trois parcours réels :
- la connexion protégée par reCAPTCHA v2 ;
- le parcours de paiement d'une boutique en ligne ;
- l'exécution planifiée de la suite dans GitHub Actions.
Structure du projet
tests/
├── conftest.py # Shared fixtures
├── helpers/
│ ├── captcha.py # CaptchaAI integration
│ └── browser.py # Selenium helpers
├── test_login.py # Login flow tests
├── test_checkout.py # Checkout flow tests
└── pytest.ini # Config
Chaque fichier porte une responsabilité claire, ce qui garde les scénarios lisibles :
helpers/captcha.py: tout l'échange avec l'API CaptchaAI ;helpers/browser.py: les utilitaires Selenium ;conftest.py: les fixtures partagées entre les tests ;test_*.py: les scénarios de bout en bout, débarrassés de la mécanique de résolution.
Le helper de résolution CaptchaAI
# tests/helpers/captcha.py
import requests
import time
import os
class CaptchaTestHelper:
"""Solve CAPTCHAs during automated tests."""
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY required for CAPTCHA tests")
def solve_recaptcha(self, sitekey, pageurl):
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()
if result.get("status") != 1:
raise RuntimeError(f"Submit failed: {result.get('request')}")
task_id = result["request"]
time.sleep(15)
for _ in range(24):
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(data["request"])
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout in test")
def inject_token(self, driver, token):
"""Inject solved token into Selenium browser."""
driver.execute_script(
'document.getElementById("g-recaptcha-response").value = arguments[0];',
token,
)
# Trigger callback if available
driver.execute_script("""
if (typeof ___grecaptcha_cfg !== 'undefined') {
var clients = ___grecaptcha_cfg.clients;
for (var key in clients) {
var client = clients[key];
for (var prop in client) {
var val = client[prop];
if (val && typeof val === 'object') {
for (var inner in val) {
if (typeof val[inner] === 'function') {
val[inner](arguments[0]);
return;
}
}
}
}
}
}
""", token)
Le helper gère tout l'échange avec l'API : envoi du sitekey et de l'URL à in.php avec la méthode userrecaptcha, puis interrogation de res.php en boucle (polling) jusqu'au token. La boucle réessaie jusqu'à 24 fois avec une pause de 5 s, puis lève un TimeoutError — de quoi absorber les résolutions lentes sans bloquer indéfiniment la suite. inject_token écrit ensuite la réponse dans le champ g-recaptcha-response et déclenche le callback reCAPTCHA v2. La clé vient de CAPTCHAAI_API_KEY : jamais en dur dans le dépôt.
Les fixtures pytest
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from helpers.captcha import CaptchaTestHelper
@pytest.fixture(scope="session")
def captcha_solver():
return CaptchaTestHelper()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope="session")
def base_url():
return "https://staging.example.com"
Le scope="session" du solveur évite de reconstruire le helper à chaque cas, tandis que le navigateur en scope="function" garantit un état propre à chaque test. Les options --no-sandbox / --disable-dev-shm-usage tiennent de façon fiable sur un runner CI ou une VM OVHcloud.
Tester la connexion protégée par CAPTCHA
# tests/test_login.py
import pytest
from selenium.webdriver.common.by import By
class TestLogin:
def test_valid_login_with_captcha(self, browser, captcha_solver, base_url):
"""Test that login succeeds when CAPTCHA is solved correctly."""
browser.get(f"{base_url}/login")
# Fill form
browser.find_element(By.ID, "email").send_keys("test@example.com")
browser.find_element(By.ID, "password").send_keys("testpassword123")
# Solve CAPTCHA
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit
browser.find_element(By.ID, "login-btn").click()
# Assert redirect to dashboard
assert "/dashboard" in browser.current_url
assert browser.find_element(By.CLASS_NAME, "welcome-message")
def test_invalid_credentials_with_captcha(self, browser, captcha_solver, base_url):
"""Test that wrong credentials show error even with valid CAPTCHA."""
browser.get(f"{base_url}/login")
browser.find_element(By.ID, "email").send_keys("wrong@example.com")
browser.find_element(By.ID, "password").send_keys("wrongpass")
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
browser.find_element(By.ID, "login-btn").click()
error = browser.find_element(By.CLASS_NAME, "error-message")
assert "Invalid" in error.text
Le premier scénario remplit le formulaire, lit le data-sitekey, résout le défi puis vérifie la redirection. Le second confirme qu'un mauvais mot de passe échoue même avec un CAPTCHA valide : la résolution ne doit jamais masquer un bug d'authentification.
Tester le parcours de paiement
# tests/test_checkout.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestCheckout:
def test_checkout_flow_with_captcha(self, browser, captcha_solver, base_url):
"""Full checkout flow: add item, fill form, solve CAPTCHA, confirm."""
# Add item to cart
browser.get(f"{base_url}/products/test-item")
browser.find_element(By.ID, "add-to-cart").click()
# Go to checkout
browser.get(f"{base_url}/checkout")
# Fill shipping
browser.find_element(By.ID, "address").send_keys("123 Test St")
browser.find_element(By.ID, "city").send_keys("Test City")
browser.find_element(By.ID, "zip").send_keys("12345")
# Solve CAPTCHA on checkout page
captcha_el = browser.find_element(By.CLASS_NAME, "g-recaptcha")
sitekey = captcha_el.get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit order
browser.find_element(By.ID, "place-order").click()
# Wait for confirmation
wait = WebDriverWait(browser, 15)
confirmation = wait.until(
EC.presence_of_element_located((By.CLASS_NAME, "order-confirmation"))
)
assert "Thank you" in confirmation.text
Le parcours de paiement enchaîne panier, adresse et résolution du CAPTCHA avant la confirmation de commande. C'est le scénario le plus proche d'un flux e-commerce réel, et celui où un CAPTCHA non géré fait le plus de dégâts. Attendez la confirmation avec un WebDriverWait plutôt qu'un sleep fixe, pour absorber la latence réseau du runner.
Configurer pytest et les marqueurs
# tests/pytest.ini
[pytest]
markers =
captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short
Le marqueur captcha isole les tests qui consomment une résolution réelle. Ils s'exécutent à trois moments distincts :
- jamais par défaut en local, où
pytest -m "not captcha"les écarte ; - à la demande, avant une mise en production ;
- automatiquement, dans l'exécution CI hebdomadaire.
Exécuter le pipeline dans GitHub Actions
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6 AM
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pytest selenium requests
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: pytest tests/ -m captcha -v
Le workflow lance la suite au push sur main et une fois par semaine via cron. Ce déclenchement hebdomadaire sert de test de non-régression : si un site change son intégration reCAPTCHA, vous le voyez avant vos utilisateurs. La clé API passe par les secrets du dépôt, jamais par le YAML en clair, et un runner ubuntu-latest avec Chrome épinglé reproduit fidèlement votre environnement local.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| L'injection du token échoue | Le textarea g-recaptcha-response est introuvable |
Vérifiez l'ID de l'élément ou ciblez querySelector('[name="g-recaptcha-response"]') |
| Les tests passent en local mais échouent en CI | Version de Chrome différente | Épinglez la version de Chrome dans la configuration CI |
| Aucun CAPTCHA en staging | L'environnement de staging désactive le CAPTCHA | Activez le CAPTCHA dans la configuration de l'environnement de test |
| Timeout pendant l'attente de la résolution | Réseau lent sur le runner CI | Portez le délai d'interrogation (polling) à 180 s |
FAQ
Combien coûte l'exécution de tests CAPTCHA en continu ?
La facturation CaptchaAI se fait au thread, pas à la résolution : une petite suite E2E lancée une fois par jour tient dans le plan BASIC ($15/mois, 5 threads), avec des résolutions illimitées par thread. Réservez les tests marqués captcha aux exécutions nécessaires.
Comment stocker ma clé API CaptchaAI dans GitHub Actions ?
Ajoutez-la comme secret de dépôt (Settings → Secrets and variables → Actions), puis exposez-la au job via env: CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}. Elle n'apparaît ni dans les logs ni dans le workflow versionné.
Pourquoi mes tests passent-ils en local mais échouent-ils en CI ?
Le plus souvent, c'est une version de Chrome différente ou un runner plus lent. Épinglez Chrome, augmentez le délai d'interrogation (polling) et vérifiez que le CAPTCHA est bien activé sur le staging testé.
Faut-il résoudre le CAPTCHA ou le désactiver en staging ?
Les deux se complètent. Désactivez le CAPTCHA pour les tests fonctionnels rapides, et gardez au moins un scénario qui résout un vrai CAPTCHA via CaptchaAI pour valider l'intégration de bout en bout avant la mise en production.
Guides connexes
Ne laissez plus un CAPTCHA interrompre vos tests — ouvrez un compte CaptchaAI.