Ein reCAPTCHA v2 oder Cloudflare Turnstile auf dem Anmeldeformular sollte Ihre Regressionssuite nicht ausbremsen. CaptchaAI löst die Abfrage per API und liefert ein gültiges Token zurück, das Ihr Test in das Formular einträgt – der Registrierungsflow läuft in Staging wie in Produktion durch, ganz ohne manuelles Eingreifen.
Das Wesentliche: Testnutzer synthetisch erzeugen, das CAPTCHA im laufenden Selenium-Test lösen, Token einfügen, absenden und den Erfolg verifizieren – inklusive Randfällen wie doppelter E-Mail oder schwachem Passwort.
Warum das CAPTCHA Ihre Registrierungstests blockiert
Anmeldeformulare sind fast immer CAPTCHA-geschützt, um Bot-Accounts fernzuhalten. Genau dieser Schutz greift aber auch, wenn ein Headless-Testrunner das Formular ausfüllt: Für den Server sieht die Automatisierung aus wie ein Bot, die Abfrage erscheint, und der Test hängt vor dem Absenden fest. Ohne eine programmatische CAPTCHA-Lösung lässt sich der wichtigste Pfad Ihrer Anwendung – die Kontoerstellung – schlicht nicht durchgängig prüfen.
Ein belastbares QA-Setup muss trotzdem:
- den Anmeldevorgang in Staging und Produktion verifizieren,
- Randfälle abdecken (doppelte E-Mail, schwaches Passwort, fehlende Felder),
- bei jedem Deployment Regressionstests fahren,
- prüfen, dass die CAPTCHA-Platzierung das Formular nicht beschädigt.
Welche CAPTCHA-Typen auf Registrierungsformularen erscheinen
In der Praxis begegnen Ihnen fast immer zwei Typen, gelegentlich ein dritter. CaptchaAI deckt die relevanten Varianten ab; einige verbreitete Systeme werden bewusst nicht unterstützt.
| CAPTCHA-Typ | Auf Anmeldeformularen | Von CaptchaAI gelöst |
|---|---|---|
| reCAPTCHA v2 | sehr häufig | ✅ |
| Cloudflare Turnstile | häufig | ✅ |
| reCAPTCHA v3 | gelegentlich | ✅ |
| Bild-/Raster-CAPTCHA | selten | ✅ |
| hCaptcha | gelegentlich | ❌ nicht unterstützt |
Voraussetzungen
Für das Beispiel benötigen Sie:
- Python 3.10 oder neuer mit installiertem
seleniumundrequests, - einen lokalen
chromedriverpassend zu Ihrer Chrome-Version, - ein CaptchaAI-Konto samt API-Schlüssel (das Guthaben genügt bereits ab dem BASIC-Tarif),
- Zugriff auf eine eigene Staging-Instanz mit Registrierungsformular.
Testarchitektur
Der Ablauf folgt einer klaren Pipeline. Jeder Schritt ist isoliert testbar, sodass ein Fehler eindeutig zugeordnet werden kann:
- Ein Generator erzeugt einen eindeutigen, synthetischen Testnutzer.
- Der Test füllt das Formular aus und deckt dabei die Randfälle ab.
- CaptchaAI löst das erkannte CAPTCHA und liefert das Token.
- Der Test verifiziert, ob die Registrierung erwartungsgemäß aus- oder eingegangen ist.
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
Umsetzung
Die Umsetzung gliedert sich in zwei Bausteine: einen Generator für Testnutzer und den eigentlichen Tester, der Browsersteuerung, CAPTCHA-Lösung und Ergebnisprüfung zusammenführt.
Testdatengenerator
Damit jeder Lauf reproduzierbar bleibt und keine Kollisionen mit vorhandenen Konten entstehen, erzeugt der Generator pro Testnutzer eine eindeutige Kombination aus Zeitstempel und Zufallszeichen. Die E-Mail-Domain (testmail.example.com) ist bewusst nicht real – so bleiben die Daten synthetisch.
import random
import string
import time
class TestUser:
def __init__(self, prefix="test"):
ts = int(time.time())
rand = ''.join(random.choices(string.ascii_lowercase, k=4))
self.first_name = f"{prefix}_{rand}"
self.last_name = "User"
self.email = f"{prefix}_{ts}_{rand}@testmail.example.com"
self.username = f"{prefix}_{ts}_{rand}"
self.password = f"Test!{ts}{rand.upper()}"
def as_dict(self):
return {
"first_name": self.first_name,
"last_name": self.last_name,
"email": self.email,
"username": self.username,
"password": self.password,
}
Registrierungstester
Der Kern besteht aus zwei Klassen. CaptchaSolver kapselt den API-Aufruf an CaptchaAI: Die Aufgabe geht per in.php ein, danach fragt der Test das Ergebnis über res.php ab, bis ein Token vorliegt. RegistrationTester steuert den Browser, füllt die Felder, erkennt den CAPTCHA-Typ am DOM (.cf-turnstile für Turnstile, [data-sitekey] für reCAPTCHA) und trägt das gelöste Token in das passende Feld ein.
import time
import requests
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
class CaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve_recaptcha(self, sitekey, pageurl):
return self._solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
})
def solve_turnstile(self, sitekey, pageurl):
return self._solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
def _solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(result["request"])
raise TimeoutError("Timed out")
class RegistrationTester:
def __init__(self, api_key, base_url):
self.solver = CaptchaSolver(api_key)
self.base_url = base_url
self.driver = webdriver.Chrome()
self.wait = WebDriverWait(self.driver, 10)
self.results = []
def _fill(self, selector, value):
el = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
el.clear()
el.send_keys(value)
def _solve_captcha(self):
html = self.driver.page_source
page_url = self.driver.current_url
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
if turnstile:
sitekey = turnstile[0].get_attribute("data-sitekey")
token = self.solver.solve_turnstile(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha and "recaptcha" in html.lower():
sitekey = recaptcha[0].get_attribute("data-sitekey")
token = self.solver.solve_recaptcha(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
def _get_errors(self):
"""Collect any visible error messages on the page."""
error_selectors = [
".error", ".alert-danger", ".form-error",
"[role='alert']", ".validation-error",
]
errors = []
for sel in error_selectors:
for el in self.driver.find_elements(By.CSS_SELECTOR, sel):
text = el.text.strip()
if text:
errors.append(text)
return errors
def _check_success(self):
"""Check if registration succeeded."""
html = self.driver.page_source.lower()
url = self.driver.current_url.lower()
success_indicators = [
"welcome", "account created", "verify your email",
"registration successful", "thank you for registering",
]
return any(ind in html or ind in url for ind in success_indicators)
# --- Test Cases ---
def test_valid_registration(self):
"""Test: Valid registration should succeed."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='firstName'], #first-name", user.first_name)
self._fill("[name='lastName'], #last-name", user.last_name)
self._fill("[name='email'], #email", user.email)
self._fill("[name='username'], #username", user.username)
self._fill("[name='password'], #password", user.password)
confirm_fields = self.driver.find_elements(By.CSS_SELECTOR, "[name='confirmPassword'], #confirm-password")
if confirm_fields:
confirm_fields[0].clear()
confirm_fields[0].send_keys(user.password)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
success = self._check_success()
self.results.append({
"test": "valid_registration",
"passed": success,
"user": user.email,
"errors": self._get_errors() if not success else [],
})
return success
def test_duplicate_email(self):
"""Test: Duplicate email should show error."""
user = TestUser()
# First registration
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
# Second registration with same email
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", "Duplicate")
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
has_error = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "duplicate_email",
"passed": has_error,
"errors": errors,
})
return has_error
def test_weak_password(self):
"""Test: Weak password should be rejected."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", "123") # Weak password
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
rejected = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "weak_password",
"passed": rejected,
"errors": errors,
})
return rejected
def run_all(self):
"""Run all registration tests."""
tests = [
self.test_valid_registration,
self.test_duplicate_email,
self.test_weak_password,
]
for test_fn in tests:
try:
test_fn()
except Exception as e:
self.results.append({
"test": test_fn.__name__,
"passed": False,
"errors": [str(e)],
})
return self.results
def report(self):
passed = sum(1 for r in self.results if r["passed"])
total = len(self.results)
lines = [f"Registration Tests: {passed}/{total} passed", "-" * 40]
for r in self.results:
status = "PASS" if r["passed"] else "FAIL"
lines.append(f" [{status}] {r['test']}")
if r.get("errors"):
for err in r["errors"]:
lines.append(f" {err}")
return "\n".join(lines)
def close(self):
self.driver.quit()
Zur Token-Gültigkeit:
_solve_captcha()steht bewusst unmittelbar vor dem Klick auf den Submit-Button. reCAPTCHA- und Turnstile-Tokens laufen nach rund 120 Sekunden ab – lösen Sie also erst kurz vor dem Absenden.
Ausführung
Ein Lauf instanziiert den Tester mit Ihrem API-Schlüssel und der Staging-Basis-URL, führt alle Fälle aus und gibt einen kompakten Bericht aus. Der finally-Block schließt den Browser zuverlässig, auch wenn ein Test scheitert.
Ein vollständiger Durchlauf besteht aus drei Schritten:
- Tester mit API-Schlüssel und Staging-URL instanziieren.
run_all()aufrufen, um alle Testfälle nacheinander abzuarbeiten.- Den Bericht ausgeben und den Browser im
finally-Block schließen.
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
Ausgabe:
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
Integration in Pytest
In einer bestehenden Testsuite lässt sich der Tester über eine Fixture einbinden. scope="module" startet den Browser einmal pro Modul und schließt ihn nach dem letzten Test – das spart Startzeit gegenüber einer Fixture pro Testfall.
import pytest
@pytest.fixture(scope="module")
def tester():
t = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
yield t
t.close()
def test_valid_registration(tester):
assert tester.test_valid_registration(), "Valid registration should succeed"
def test_duplicate_email_rejected(tester):
assert tester.test_duplicate_email(), "Duplicate email should be rejected"
def test_weak_password_rejected(tester):
assert tester.test_weak_password(), "Weak password should be rejected"
Testdaten, DSGVO und CI in der Praxis
In deutschen und österreichischen Teams laufen solche Registrierungstests meist in GitLab CI oder GitHub Actions. Hinterlegen Sie den API-Schlüssel als geschützte CI-Variable statt im Repository und aktivieren Sie den Headless-Modus (options.add_argument("--headless")), damit die Suite ohne Display durchläuft. Für breite Testmatrizen skaliert CaptchaAI über Threads: Der BASIC-Tarif (ab 15 $/Monat, 5 Threads) erlaubt fünf gleichzeitige Lösungen, ADVANCE (90 $/Monat, 50 Threads) deckt umfangreichere Parallelläufe ab – abgerechnet wird pro Thread, nicht pro Lösung.
DSGVO-Hinweis: Verwenden Sie ausschließlich synthetische Nutzer wie im
TestUser-Generator – keine echten Namen, E-Mail-Adressen oder Bestandsdaten. So testen Sie realistische Registrierungsflows, etwa in einer Shopware- oder JTL-Instanz, ohne personenbezogene Daten in die Testumgebung zu ziehen.
Als kurze Checkliste für sauberes Testen:
- ausschließlich synthetische Testdaten verwenden,
- Konten in einer isolierten Staging-Umgebung anlegen,
- nach dem Lauf per Teardown wieder entfernen.
Fehlerbehebung beim CAPTCHA-Handling
Die häufigsten Stolpersteine und ihre Lösung im Überblick:
| Problem | Ursache | Lösung |
|---|---|---|
| Registrierung klappt, der Test meldet aber einen Fehlschlag | Erfolgsindikator passt nicht | Ergänzen Sie den Erfolgstext Ihrer Seite in _check_success() |
| CAPTCHA wird nicht erkannt | CAPTCHA lädt verzögert nach | time.sleep(2) vor _solve_captcha() einfügen |
| Felder nicht gefunden | Abweichende HTML-Struktur | CSS-Selektoren an Ihre Seite anpassen |
| Token abgelaufen | Zu früh gelöst | _solve_captcha() näher an das Absenden rücken |
FAQ
Kurze Antworten auf wiederkehrende Fragen aus der Testpraxis:
Welche CAPTCHA-Typen tauchen in Registrierungsformularen auf?
Meist reCAPTCHA v2 und Cloudflare Turnstile, seltener reCAPTCHA v3 oder ein Bild-CAPTCHA. CaptchaAI deckt die relevanten Varianten ab:
- reCAPTCHA v2/v3 und Cloudflare Turnstile,
- GeeTest v3 sowie Bild- und Raster-CAPTCHAs,
- hCaptcha und FunCaptcha dagegen nicht – prüfen Sie vorab, welchen Typ Ihr Formular einsetzt.
Wie verhindere ich, dass das Token vor dem Absenden abläuft?
Lösen Sie das CAPTCHA erst unmittelbar vor dem Klick auf „Absenden“. reCAPTCHA- und Turnstile-Tokens sind nur rund 120 Sekunden gültig. Im Beispiel steht _solve_captcha() direkt vor dem Submit – halten Sie diese Reihenfolge ein, dann ist das Token beim Absenden noch frisch.
Kann ich mehrere Registrierungstests parallel ausführen?
Ja. CaptchaAI rechnet pro Thread ab, nicht pro Lösung. Der BASIC-Tarif (ab 15 $/Monat, 5 Threads) erlaubt fünf gleichzeitige Lösungen; für breite Testmatrizen in CI skalieren Sie über einen größeren Tarif wie ADVANCE (90 $/Monat, 50 Threads).
Wie gehe ich DSGVO-konform mit Testdaten um?
Verwenden Sie ausschließlich synthetische Testnutzer wie im TestUser-Generator, keine echten personenbezogenen Daten. Legen Sie die Konten in einer isolierten Staging-Umgebung an und löschen Sie sie nach dem Lauf per Teardown.
Fazit
Registrierungstests müssen am CAPTCHA nicht scheitern. Wer CaptchaAI in die Testsuite einbindet, prüft Anmeldeflows samt Randfällen zuverlässig in Staging und CI – reCAPTCHA v2, reCAPTCHA v3 und Cloudflare Turnstile inklusive. Das Token wird erst kurz vor dem Absenden gelöst, der übrige Testcode bleibt unangetastet.
Verwandte Leitfäden
Weiterführende Anleitungen im CaptchaAI-Blog:
- Formulare trotz CAPTCHA automatisiert absenden
- CaptchaAI API-Kurzreferenz
Testen Sie Registrierungsabläufe ohne CAPTCHA-Blocker – mit CaptchaAI starten.