Tutorials

Erstellen Sie mit CaptchaAI eine automatisierte Testpipeline

Sobald ein Login- oder Checkout-Formular durch ein CAPTCHA geschützt ist, bleibt jeder End-to-End-Test genau an dieser Stelle stehen. Die saubere Antwort darauf ist nicht, die Sicherheitsabfrage im Staging abzuschalten, sondern eine Pytest-Fixture, die das CAPTCHA während des Testlaufs über CaptchaAI löst und den fertigen Token direkt in das Formular einträgt.

Dieser Leitfaden baut eine solche Pipeline Schritt für Schritt auf – von der Projektstruktur über den Selenium-Helfer und die konkreten Login- und Checkout-Tests bis zum GitHub-Actions-Workflow, der die Suite in der CI ausführt. Am Ende laufen Ihre E2E-Tests unbeaufsichtigt durch, ohne dass ein CAPTCHA sie ausbremst.

Konkret entsteht dabei:

  • ein wiederverwendbarer CaptchaAI-Helfer, der reCAPTCHA v2 löst und den Token einträgt,
  • Pytest-Fixtures für Browser und Solver,
  • lauffähige Login- und Checkout-Tests,
  • ein GitHub-Actions-Workflow, der alles in der CI zusammenführt.

So ist die Test-Pipeline aufgebaut

Die Pipeline trennt drei Zuständigkeiten sauber voneinander: die CaptchaAI-Anbindung, die Selenium-Hilfsfunktionen und die eigentlichen Testfälle. Diese Aufteilung hält die Fixtures wiederverwendbar und die Testdateien schlank – neue Flows greifen einfach auf denselben Helfer zu, statt die Lösungslogik zu kopieren:

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

Der CaptchaAI-Testhelfer

Der Helfer folgt dem Zwei-Schritt-Muster der CaptchaAI-API: Er übermittelt Sitekey und Page-URL an in.php und fragt das Ergebnis anschließend an res.php ab, bis der Token bereitsteht (Polling). Die Methode inject_token schreibt den gelösten Token in das versteckte Feld g-recaptcha-response und stößt, falls vorhanden, den zugehörigen Callback an – so verhält sich die Seite genau so, als hätte ein Mensch das reCAPTCHA v2 gelöst.

Der Ablauf im Helfer besteht aus drei Schritten:

  1. Sitekey und Page-URL an in.php übermitteln und die Task-ID entgegennehmen.
  2. Das Ergebnis an res.php abfragen, bis der Token vorliegt (Polling mit kurzer Wartezeit zwischen den Versuchen).
  3. Den Token in das Feld g-recaptcha-response schreiben und den zugehörigen Callback auslösen.

Ein Token ist nur rund 120 Sekunden gültig. Deshalb löst der Helfer das CAPTCHA erst unmittelbar vor dem Absenden und nicht auf Vorrat. Abgerechnet wird bei CaptchaAI ohnehin pro Thread und nicht pro Lösung, sodass die Zahl der Testläufe die Kosten nicht in die Höhe treibt.

# 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)

Pytest-Fixtures einrichten

Zwei Fixtures bilden das Fundament. Der captcha_solver läuft im session-Scope und wird einmal pro Testlauf erzeugt, während der browser im function-Scope für jeden Test eine frische, isolierte Chrome-Instanz startet und danach wieder schließt. Für die CI ist der Browser bewusst headless konfiguriert, mit den Optionen --no-sandbox und --disable-dev-shm-usage, die in Container-Umgebungen typische Startprobleme vermeiden.

Fixture Scope Zweck
captcha_solver session einmalige CaptchaAI-Anbindung pro Lauf
browser function frische Chrome-Instanz je Test
base_url session zentrale Staging-URL

Damit sieht die conftest.py so aus:

# 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"

Login-Test mit CAPTCHA

Der erste Testfall deckt den Login-Flow ab. Er füllt E-Mail und Passwort aus, liest den Sitekey aus dem g-recaptcha-Element, lässt CaptchaAI den Token lösen, trägt ihn ein und sendet das Formular ab. Wichtig ist, dass die Suite beide Pfade prüft: den erfolgreichen Login mit gültigen Zugangsdaten und die Fehlermeldung bei falschen Daten – auch dann, wenn das CAPTCHA korrekt gelöst wurde. So stellen Sie sicher, dass die eigentliche Anwendungslogik getestet wird und nicht nur die CAPTCHA-Abfrage:

# 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

Checkout-Test im Staging

Der zweite Testfall durchläuft einen vollständigen Checkout: Artikel in den Warenkorb legen, Lieferadresse ausfüllen, das CAPTCHA auf der Checkout-Seite lösen und die Bestellung abschließen. Alle Aktionen laufen ausschließlich gegen Ihre eigene Staging-Umgebung (base_url zeigt auf staging.example.com) – es geht um die Qualitätssicherung des eigenen Shops, nicht um automatisierte Käufe bei fremden Anbietern. Wer mit Shopware oder JTL arbeitet, bildet denselben Ablauf gegen die jeweilige Test-Instanz ab.

Hinweis: Alle Beispiele richten sich ausschließlich an eigene oder ausdrücklich autorisierte Staging-Umgebungen. Sobald Datenextraktion mit Personenbezug ins Spiel kommt, prüfen Sie zusätzlich Ihre DSGVO-Grundlagen – IP-Adressen gelten in der EU als personenbezogene Daten.

Der Testfall im Detail:

# 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

Pytest-Konfiguration und Marker

Ein eigener Marker captcha kennzeichnet alle Tests, die echte Lösungen anfordern. Damit lassen sich die kostenrelevanten Tests gezielt ansteuern oder überspringen – lokal während der Entwicklung etwa pytest -m "not captcha", in der geplanten CI-Ausführung dagegen genau diese Gruppe:

# tests/pytest.ini
[pytest]
markers =
    captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short

GitHub-Actions-Workflow

Zum Abschluss übernimmt die CI die Ausführung. Der Workflow löst bei jedem Push auf main aus und zusätzlich einmal wöchentlich per Cron – so fällt eine kaputte CAPTCHA-Integration auch dann auf, wenn gerade niemand deployt. Der API-Schlüssel liegt als GitHub-Actions-Secret CAPTCHAAI_API_KEY vor und wird nie im Klartext committet:

# .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

In vielen DACH-Teams läuft die CI nicht auf GitHub Actions, sondern auf GitLab CI. Der Ablauf bleibt identisch: Sie hinterlegen CAPTCHAAI_API_KEY als maskierte CI/CD-Variable und rufen im script-Abschnitt denselben pytest-Befehl auf. Wer eigene Runner betreibt – etwa auf einem Hetzner- oder netcup-Server – erhält zusätzlich stabilere Laufzeiten als auf geteilten Runnern.

Typische Probleme und Lösungen

Problem Ursache Lösung
Das Einfügen des Tokens schlägt fehl Textfeld nicht gefunden Element-ID prüfen oder querySelector('[name="g-recaptcha-response"]') verwenden
Tests laufen lokal, scheitern in der CI Abweichende Chrome-Version Chrome-Version im CI-Setup festpinnen
Im Staging erscheint kein CAPTCHA Staging deaktiviert CAPTCHAs CAPTCHA in der Staging-Konfiguration aktivieren
Zeitüberschreitung beim Warten auf die Lösung Langsames Netzwerk in der CI Polling-Timeout auf 180 Sekunden erhöhen

Häufige Fragen

Welche CAPTCHA-Typen deckt CaptchaAI in einer Test-Pipeline ab?

reCAPTCHA v2 und v3 (inklusive Enterprise), Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild- und Raster-CAPTCHAs. hCaptcha, FunCaptcha und GeeTest v4 werden nicht unterstützt; GeeTest v4 ist als „bald verfügbar" angekündigt. Für dieses Tutorial genügt der userrecaptcha-Flow für reCAPTCHA v2.

Was kostet eine solche Test-Pipeline im Monat?

CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – jeder Tarif enthält unbegrenzte Lösungen pro Thread. Da E2E-Tests meist sequentiell laufen, reicht in der Regel der BASIC-Tarif (15 $/Monat, 5 Threads). Die Anzahl der Testläufe treibt die Kosten also nicht in die Höhe.

Funktioniert der Ansatz auch mit Playwright statt Selenium?

Ja. Der CaptchaTestHelper ist framework-unabhängig, weil er nur mit der HTTP-API von CaptchaAI spricht. Statt driver.execute_script verwenden Sie in Playwright page.evaluate, um den Token in das Feld g-recaptcha-response zu schreiben und den Callback auszulösen.

Warum laufen die Tests lokal, scheitern aber in der CI?

Meistens liegt es an einer abweichenden Chrome-Version oder an fehlenden Headless-Optionen im Container. Pinnen Sie die Chrome-Version im CI-Setup fest und behalten Sie --no-sandbox sowie --disable-dev-shm-usage bei. Bei langsamen Runnern erhöhen Sie zusätzlich das Polling-Timeout.

Wie halte ich die kostenrelevanten Tests aus der lokalen Entwicklung heraus?

Markieren Sie sie mit @pytest.mark.captcha und starten Sie lokal pytest -m "not captcha". In der CI führen Sie umgekehrt gezielt pytest -m captcha aus, sodass echte Lösungen nur im geplanten Lauf anfallen.


Fazit

Mit dieser Pipeline lösen Ihre automatisierten Tests CAPTCHAs zuverlässig im Hintergrund, statt an ihnen zu scheitern. Drei Bausteine tragen das Setup:

  • Zentraler Helfer: löst reCAPTCHA v2 über CaptchaAI und trägt den Token ins Formular ein.
  • Saubere Fixtures: trennen Browser, Solver und Testfälle voneinander.
  • CI-Anbindung: führt die Suite planbar aus – bei jedem Push und wöchentlich per Cron.

Weiterführende Leitfäden


Lassen Sie CAPTCHAs Ihre Testsuite nicht länger blockieren – starten Sie mit CaptchaAI.

Kommentare sind für diesen Artikel deaktiviert.