Ein Testlauf, der sauber durchläuft und dann am Login-Formular hängen bleibt, weil dort ein reCAPTCHA sitzt – dieses Bild kennt jedes QA-Team, das echte Nutzerflüsse in der Pipeline abbildet. Die Antwort darauf ist unkompliziert: Ihre Testsuite ruft die API von CaptchaAI auf, erhält ein gültiges Token und setzt den Ablauf fort, ganz ohne manuellen Eingriff.
CAPTCHAs sind das einzige Element eines End-to-End-Tests, das sich mit Selenium oder Playwright allein nicht bedienen lässt. Genau diese Lücke schließt ein Lösungsdienst, der als CI-Secret hinterlegt ist und bei jedem Pipeline-Lauf automatisch anspringt.
Warum CAPTCHAs CI/CD-Pipelines ausbremsen
CI/CD-Pipelines laufen ohne Zutun eines Menschen – genau das ist ihr Sinn. Ein CAPTCHA verlangt aber per Definition eine Interaktion, die ein Bot nicht leisten soll. Ohne Lösungsdienst bricht jeder End-to-End-Test ab, sobald er auf eine reCAPTCHA- oder Turnstile-Abfrage trifft, und Ihr Report färbt sich rot – obwohl mit dem eigentlichen Code alles in Ordnung ist.
Der Ausweg besteht nicht darin, CAPTCHAs in der Testumgebung einfach abzuschalten, denn das verändert genau den Pfad, den Sie prüfen wollen. Behandeln Sie die Abfrage stattdessen so, wie es echte Nutzer tun: lösen, das Token in das Formular eintragen, weitermachen. CaptchaAI übernimmt den ersten Schritt per API, Ihre Testlogik den Rest.
Welche CAPTCHA-Typen in CI-Tests abgedeckt sind
Für automatisierte Tests deckt CaptchaAI die Typen ab, die in Login-, Registrierungs- und Kontaktformularen am häufigsten auftauchen:
- reCAPTCHA v2 und v3, inklusive Enterprise
- Cloudflare Turnstile
- GeeTest v3
- klassische Bild- und Rasterbild-CAPTCHAs
Die beiden Beispiele in diesem Leitfaden nutzen reCAPTCHA v2 (Login) und Turnstile (Kontaktformular) – für die übrigen Typen bleibt das Muster gleich, es ändert sich nur der method-Parameter im API-Aufruf.
hCaptcha und FunCaptcha werden nicht unterstützt; setzt Ihre Anwendung darauf, planen Sie den betreffenden Test entsprechend ein. GeeTest v4 ist angekündigt, aber noch nicht verfügbar.
Architektur: das CAPTCHA-Handling in der Pipeline
Der Ablauf ist bei allen drei CI-Systemen identisch und lässt sich in vier Stationen zerlegen:
- Ein Push oder Merge-Request stößt den Runner an.
- Der Runner startet die E2E-Tests in einem Headless-Chrome.
- Trifft ein Test auf ein CAPTCHA, ruft er die CaptchaAI-API auf und wartet auf das Token.
- Mit dem eingesetzten Token sendet der Test das Formular ab und schreibt das Ergebnis in den Report.
┌──────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Git Push │────▶│ CI Runner │────▶│ E2E Tests │────▶│ Test Report │
│ │ │ (headless │ │ + CAPTCHA │ │ │
│ │ │ Chrome) │ │ solving │ │ │
└──────────────┘ └──────────────┘ └────────────┘ └──────────────┘
│
▼
┌────────────┐
│ CaptchaAI │
│ API │
└────────────┘
Ein wiederverwendbarer CAPTCHA-Helfer für CI
Kapseln Sie die API-Aufrufe in einer kleinen Klasse, die Sie in jedem Test wiederverwenden. Sie liest den API-Schlüssel aus der Umgebungsvariable CAPTCHAAI_API_KEY, übermittelt die Abfrage an in.php und fragt das Ergebnis anschließend an res.php ab, bis das Token vorliegt oder das Timeout greift. Das anfängliche initial_wait spart unnötige Anfragen, während der Dienst noch rechnet:
import os
import time
import requests
class CICaptchaSolver:
"""CAPTCHA solver designed for CI environments."""
BASE = "https://ocr.captchaai.com"
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY not set")
def solve(self, params, initial_wait=10, timeout=120):
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(f"CAPTCHA submit failed: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
deadline = time.time() + timeout
while time.time() < deadline:
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(f"CAPTCHA solve failed: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out in CI")
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,
})
Integration in pytest
Mit zwei Fixtures ist die Anbindung erledigt:
- eine session-weite Fixture für den Solver – die gesamte Suite teilt sich eine Instanz
- eine funktionsweite Fixture für den Browser – jeder Test bekommt einen frischen, sauberen Headless-Chrome
conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="session")
def captcha_solver():
return CICaptchaSolver()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=options)
driver.set_window_size(1920, 1080)
yield driver
driver.quit()
Die Testdatei
Der eigentliche Test folgt dem Ablauf echter Nutzer: Felder ausfüllen, CAPTCHA lösen, Token in das Antwortfeld eintragen, absenden, Ergebnis prüfen. Beachten Sie den Unterschied bei den Feldnamen – reCAPTCHA erwartet das Token in g-recaptcha-response, Turnstile dagegen in cf-turnstile-response:
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestLoginFlow:
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
LOGIN_URL = "https://staging.example.com/login"
def test_login_with_captcha(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
# Fill credentials
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("testpass123")
# Solve CAPTCHA
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
# Submit
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
# Verify login success
assert "dashboard" in browser.current_url.lower()
def test_login_wrong_password(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("wrongpass")
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
error = browser.find_element(By.CSS_SELECTOR, ".error-message")
assert error.is_displayed()
class TestContactForm:
SITEKEY = "0x4AAAA..."
FORM_URL = "https://staging.example.com/contact"
def test_contact_form_submission(self, browser, captcha_solver):
browser.get(self.FORM_URL)
browser.find_element(By.ID, "name").send_keys("CI Test")
browser.find_element(By.ID, "email").send_keys("ci@test.com")
browser.find_element(By.ID, "message").send_keys("Automated CI test")
token = captcha_solver.solve_turnstile(self.SITEKEY, self.FORM_URL)
browser.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
browser.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
)
GitHub Actions: E2E-Tests mit CAPTCHA-Lösung
Der Workflow installiert Python und Chrome, reicht den API-Schlüssel über secrets.CAPTCHAAI_API_KEY in die Umgebung und lädt den HTML-Report auch dann hoch, wenn ein Test fehlschlägt (if: always()). So sehen Sie nach jedem Lauf sofort, woran es lag:
name: E2E Tests with CAPTCHA
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: Install ChromeDriver
uses: nanasess/setup-chromedriver@v2
- name: Install dependencies
run: |
pip install selenium requests pytest pytest-html
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: |
pytest tests/e2e/ -v --html=report.html --self-contained-html
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-report
path: report.html
GitLab CI konfigurieren
GitLab CI ist in vielen deutschen Unternehmen der Standard, oft selbst gehostet auf eigener Hardware oder bei Anbietern wie Hetzner. Statt Chrome direkt im Job zu installieren, nutzen Sie den offiziellen Selenium-Service-Container und verbinden sich über SELENIUM_REMOTE_URL:
e2e_tests:
stage: test
image: python:3.11
services:
- selenium/standalone-chrome:latest
variables:
SELENIUM_REMOTE_URL: "http://selenium__standalone-chrome:4444/wd/hub"
script:
- pip install selenium requests pytest
- pytest tests/e2e/ -v
artifacts:
when: always
reports:
junit: report.xml
Jenkins-Pipeline
Auf Jenkins liegt der API-Schlüssel in den Credentials und wird über credentials('captchaai-api-key') in die Umgebung gereicht. Der post-Block macht die JUnit-Reports auch bei einem roten Lauf sichtbar:
pipeline {
agent any
environment {
CAPTCHAAI_API_KEY = credentials('captchaai-api-key')
}
stages {
stage('Setup') {
steps {
sh 'pip install selenium requests pytest'
}
}
stage('E2E Tests') {
steps {
sh 'pytest tests/e2e/ -v --junitxml=results.xml'
}
}
}
post {
always {
junit 'results.xml'
}
}
}
Kosten in der CI-Pipeline im Griff behalten
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – jeder Tarif enthält unbegrenzt viele Lösungen pro Thread im Abrechnungsmonat. Für CI passt das gut, weil CAPTCHA-Lösungen dort in Schüben auftreten und die Leitung zwischen den Läufen frei ist. Der Einstiegstarif BASIC (15 $/Monat, 5 Threads) deckt die typische Last einer nächtlichen E2E-Suite bequem ab; erst wenn viele Pipelines gleichzeitig laufen, lohnt ein größerer Tarif wie ADVANCE (90 $/Monat, 50 Threads). Preise in US-Dollar.
Zwei einfache Stellschrauben halten die Kosten niedrig:
- Nicht bei jedem Commit lösen – CAPTCHA-Tests laufen nur beim nächtlichen Durchlauf oder beim Merge auf
main, nicht bei jedem PR-Build. - Vor dem Lauf das Guthaben prüfen – so überspringt die Suite sauber, statt mitten im Durchlauf zu scheitern.
Achten Sie außerdem darauf, in CI-Logs keine echten personenbezogenen Daten abzulegen – synthetische Testkonten sind unter DSGVO-Gesichtspunkten die sichere Wahl.
Nur lösen, wenn nötig
Ein Flag entscheidet, ob CAPTCHA-Tests überhaupt laufen. Bei reinen PR-Builds bleibt es aus, beim nächtlichen Lauf gegen main schalten Sie es ein:
import os
def should_run_captcha_tests():
"""Skip CAPTCHA tests in certain environments."""
if os.environ.get("SKIP_CAPTCHA_TESTS"):
return False
if not os.environ.get("CAPTCHAAI_API_KEY"):
return False
return True
# In test
import pytest
@pytest.mark.skipif(
not should_run_captcha_tests(),
reason="CAPTCHA tests disabled or API key not set"
)
class TestWithCaptcha:
def test_login(self, browser, captcha_solver):
pass
Guthaben vor dem Lauf prüfen
Eine autouse-Fixture fragt das Guthaben einmal pro Session ab und überspringt die gesamte Suite, statt sie mitten im Lauf scheitern zu lassen:
@pytest.fixture(scope="session", autouse=True)
def check_captcha_balance(captcha_solver):
import requests
resp = requests.get(
f"{captcha_solver.BASE}/res.php",
params={"key": captcha_solver.api_key, "action": "getbalance"},
)
balance = float(resp.text)
if balance < 0.50:
pytest.skip(f"CaptchaAI balance too low: ${balance:.2f}")
Typische Fehler und ihre Behebung
| Problem | Ursache | Behebung |
|---|---|---|
CAPTCHAAI_API_KEY not set |
Secret nicht konfiguriert | Schlüssel in den CI-Secrets hinterlegen |
| Chrome stürzt im CI ab | Flag --no-sandbox fehlt |
Headless-Chrome-Flags ergänzen |
| Tests laufen lokal, scheitern im CI | Abweichende Browserversion | Chrome-Version im CI pinnen |
| CAPTCHA-Timeout | CI-Netzwerk ist langsam | Parameter timeout erhöhen |
| Tests werden teuer | Zu viele Lösungen pro Lauf | SKIP_CAPTCHA_TESTS für PR-Builds nutzen |
Häufige Fragen
Welche CAPTCHA-Typen kann CaptchaAI in CI-Tests lösen?
reCAPTCHA v2 und v3, Cloudflare Turnstile, GeeTest v3 sowie Bild- und Raster-CAPTCHAs. hCaptcha und FunCaptcha werden nicht unterstützt, GeeTest v4 ist bald verfügbar. Für jeden Typ ändert sich lediglich der method-Parameter im API-Aufruf.
Was kostet das Lösen von CAPTCHAs in einer CI-Pipeline?
Die Abrechnung erfolgt pro Thread, nicht pro Lösung, mit unbegrenzten Lösungen pro Thread. BASIC (15 $/Monat, 5 Threads) reicht für die meisten nächtlichen E2E-Suiten; parallele Pipelines profitieren von ADVANCE (90 $/Monat, 50 Threads).
Warum bestehen meine Tests lokal, scheitern aber im CI?
Meist liegt es an der Umgebung: eine andere Chrome-Version, fehlende Headless-Flags wie --no-sandbox oder ein langsameres Netzwerk mit zu knappem Timeout. Pinnen Sie die Browserversion und erhöhen Sie bei Bedarf den timeout-Parameter.
Kann ich mehrere CAPTCHA-Tests gleichzeitig ausführen?
Ja. Jeder Test holt sich sein eigenes Token, und CaptchaAI verarbeitet gleichzeitige Anfragen bis zur Zahl Ihrer Threads. Mit pytest-xdist verteilen Sie die Tests auf mehrere Prozesse.
Weiterführende Leitfäden
- Registrierungsfluss automatisiert testen
- API-Kurzreferenz für CaptchaAI
Hinterlegen Sie Ihren API-Schlüssel als CI-Secret und lassen Sie Ihre E2E-Tests CAPTCHAs automatisch lösen – mit CaptchaAI starten.