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:
- Sitekey und Page-URL an
in.phpübermitteln und die Task-ID entgegennehmen. - Das Ergebnis an
res.phpabfragen, bis der Token vorliegt (Polling mit kurzer Wartezeit zwischen den Versuchen). - Den Token in das Feld
g-recaptcha-responseschreiben 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.