Use Cases

Résolution de CAPTCHA pour l'assurance qualité et les tests autorisés

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 :

  1. Envoyer le sitekey et l'URL de la page à l'endpoint in.php de l'API.
  2. Interroger res.php à intervalle régulier jusqu'à ce que le token soit prêt.
  3. Renvoyer le token, qui part ensuite dans le champ g-recaptcha-response de 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-sitekey du widget ;
  • résoudre le CAPTCHA via le helper, puis écrire le token dans le champ caché g-recaptcha-response ;
  • déclencher le callback data-callback s'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


Automatisez votre pipeline d'assurance qualité et ajoutez CaptchaAI à vos tests.

Les commentaires sont désactivés pour cet article.