Vor dem Anmeldeformular eines Campus-Portals steht fast immer eine reCAPTCHA-v2-Abfrage – und genau dort bleibt die nächtliche Regressionssuite stehen, lange bevor sie Belegung, Warteliste oder Sitzplatzanzeige überhaupt erreicht. Der übliche Weg: Die Testsuite liest den sitekey aus dem HTML, lässt das Token über die CaptchaAI-API lösen und sendet es im Feld g-recaptcha-response mit dem Formular ab. Danach läuft der Testfall weiter, als hätte die Abfrage nie dazwischengestanden.
Dieser Leitfaden zeigt, an welchen Stellen eines Campus-Portals CAPTCHA-Abfragen auftauchen, wie ein Python- und ein JavaScript-Testlauf sie verarbeiten, wie viele Threads eine nächtliche Regressionssuite realistisch braucht und welche Fehlerbilder in der Praxis am häufigsten auftreten.
Vorab: nur eigene oder freigegebene Portale testen
Ein Registrierungsportal verarbeitet personenbezogene Daten – Matrikelnummern, Prüfungsergebnisse, Anschriften. Automatisierte Tests gehören deshalb in eine Staging-Instanz des eigenen Rechenzentrums oder auf ein System, für das eine schriftliche Freigabe vorliegt. Drei Punkte, die in DACH-Projekten regelmäßig auf der Checkliste stehen:
- Synthetische Testkonten statt echter Studierendendaten. Erzeugen Sie Matrikelnummern und Namen aus einem Testdatensatz; DSGVO-relevante Datensätze haben in einer Testsuite nichts zu suchen.
- Termin mit dem Rechenzentrum. Lasttests gegen ein Campus-Management-System wie HISinOne oder CAMPUSonline stimmen Sie mit dem Betrieb ab – nicht am Abend vor dem Start der Belegungsfrist.
- Eigene Kennungen für die Suite. Hängt das Portal über Shibboleth am Identity-Management der Hochschule, braucht der Testlauf eigene Accounts, damit echte Konten nicht gesperrt werden.
In allen Beispielen steht portal_url für Ihre eigene Staging-Instanz.
Wo im Registrierungsportal reCAPTCHA v2 und Turnstile auftauchen
| Schritt im Portal | CAPTCHA-Typ | Warum die Abfrage dort steht |
|---|---|---|
| Login für Studierende | reCAPTCHA v2 | schützt vor massenhaften Anmeldeversuchen |
| Kurssuche und Vorlesungsverzeichnis | reCAPTCHA v2 | begrenzt automatisierte Abfragen |
| Kurs belegen oder abmelden | reCAPTCHA v2 | verhindert automatische Belegung |
| Eintrag in die Warteliste | reCAPTCHA v2 | hält Registrierungs-Bots fern |
| Stundenplaner | Cloudflare Turnstile | schützt interaktive Werkzeuge |
| Notenauszug anfordern | reCAPTCHA v2 | prüft, ob eine Person die Anfrage stellt |
reCAPTCHA v2 dominiert also deutlich; Turnstile taucht meist nur an den interaktiven Randbereichen auf. Wie der Ablauf im Detail funktioniert, steht in der Anleitung reCAPTCHA v2 per API lösen.
reCAPTCHA v2 im Python-Testlauf lösen
Die Klasse RegistrationTester deckt vier Testfälle ab: Login, Kurssuche, Belegung und Sitzplatzverfügbarkeit. Zwei Details entscheiden über die Stabilität des Laufs:
- Alle Aufrufe laufen über eine
requests.Session(). Cookies und Sitzungszustand bleiben erhalten, sodass nicht jeder einzelne Schritt eine neue Abfrage auslöst. - Außerhalb des Logins wird erst gelöst, wenn
_has_captcha()tatsächlich eine Abfrage im HTML findet. Das spart Threads auf Seiten, die nur sporadisch prüfen.
_solve_recaptcha() übermittelt Sitekey und Page-URL mit der Methode userrecaptcha an in.php und fragt anschließend res.php im Drei-Sekunden-Takt ab. reCAPTCHA v2 löst CaptchaAI in der Regel in unter 60 Sekunden; die Schleife mit 60 Durchläufen ist entsprechend großzügig bemessen.
import requests
import time
import re
class RegistrationTester:
def __init__(self, api_key, portal_url):
self.api_key = api_key
self.portal_url = portal_url
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def test_login(self, student_id, password):
"""Test login flow with CAPTCHA handling."""
login_page = self.session.get(f"{self.portal_url}/login")
site_key = self._extract_site_key(login_page.text)
token = self._solve_recaptcha(site_key, f"{self.portal_url}/login")
response = self.session.post(f"{self.portal_url}/login", data={
"studentId": student_id,
"password": password,
"g-recaptcha-response": token
})
return {
"status": response.status_code,
"redirected": response.url != f"{self.portal_url}/login",
"success": "dashboard" in response.url or response.status_code == 200
}
def test_course_search(self, term, department, level=None):
"""Test course search returns correct results."""
params = {"term": term, "dept": department}
if level:
params["level"] = level
response = self.session.get(
f"{self.portal_url}/courses/search", params=params
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/search"
)
response = self.session.post(
f"{self.portal_url}/courses/search",
data={**params, "g-recaptcha-response": token}
)
courses = self._parse_courses(response.text)
return {
"count": len(courses),
"courses": courses,
"all_match_dept": all(
c["department"] == department for c in courses
)
}
def test_enrollment(self, course_id, section):
"""Test enrolling in a course section."""
enroll_page = self.session.get(
f"{self.portal_url}/enroll/{course_id}/{section}"
)
if self._has_captcha(enroll_page.text):
site_key = self._extract_site_key(enroll_page.text)
token = self._solve_recaptcha(
site_key,
f"{self.portal_url}/enroll/{course_id}/{section}"
)
else:
token = None
form_data = {
"courseId": course_id,
"section": section,
"confirm": "true"
}
if token:
form_data["g-recaptcha-response"] = token
response = self.session.post(
f"{self.portal_url}/enroll/submit",
data=form_data
)
return {
"status": response.status_code,
"enrolled": "success" in response.text.lower(),
"waitlisted": "waitlist" in response.text.lower(),
"error": self._extract_error(response.text)
}
def test_seat_availability(self, course_ids):
"""Test seat availability display for multiple courses."""
results = []
for course_id in course_ids:
response = self.session.get(
f"{self.portal_url}/courses/{course_id}"
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/{course_id}"
)
response = self.session.post(
f"{self.portal_url}/courses/{course_id}",
data={"g-recaptcha-response": token}
)
availability = self._parse_availability(response.text)
results.append({"course_id": course_id, **availability})
return results
def _has_captcha(self, html):
return "g-recaptcha" in html or "recaptcha" in html.lower()
def _extract_site_key(self, html):
match = re.search(r'data-sitekey="([^"]+)"', html)
if match:
return match.group(1)
raise ValueError("reCAPTCHA site key not found")
def _solve_recaptcha(self, site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("reCAPTCHA solve timed out")
def _parse_courses(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_empty(node):
return node.text.strip() if node and node.text else ""
courses = []
for row in soup.select(".course-row, tr.course"):
courses.append({
"id": row.get("data-course-id", ""),
"department": text_or_empty(row.select_one(".dept")),
"number": text_or_empty(row.select_one(".number")),
"title": text_or_empty(row.select_one(".title")),
"seats": text_or_empty(row.select_one(".seats"))
})
return courses
def _parse_availability(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_none(node):
return node.text.strip() if node and node.text else None
return {
"total_seats": text_or_none(soup.select_one(".total-seats")),
"enrolled": text_or_none(soup.select_one(".enrolled-count")),
"available": text_or_none(soup.select_one(".available-seats")),
"waitlist": text_or_none(soup.select_one(".waitlist-count"))
}
def _extract_error(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
error = soup.select_one(".error-message, .alert-danger")
return error.text.strip() if error else None
Die komplette Suite in JavaScript ausführen
Für End-to-End-Läufe in Node.js bündelt RegistrationTestSuite fünf Testfälle – Login, Kurssuche, Belegung, Abmeldung und Wartelistenlogik – und fängt Fehler pro Testfall ab. Ein einzelner Fehlschlag bricht den Lauf also nicht ab; generateReport() liefert am Ende Anzahl, Pass-Quote und Details. Das passt gut in eine GitLab-CI-Stage, die nachts gegen die Staging-Instanz läuft.
class RegistrationTestSuite {
constructor(apiKey, portalUrl) {
this.apiKey = apiKey;
this.portalUrl = portalUrl;
this.results = [];
}
async runAll(testCredentials) {
const tests = [
() => this.testLogin(testCredentials),
() => this.testCourseSearch('Fall2025', 'CS'),
() => this.testEnrollment('CS101', '001'),
() => this.testDropCourse('CS101', '001'),
() => this.testWaitlistFlow('CS201', '001')
];
for (const test of tests) {
try {
const result = await test();
this.results.push({ ...result, passed: true });
} catch (error) {
this.results.push({ name: test.name, error: error.message, passed: false });
}
}
return this.generateReport();
}
async solveRecaptcha(pageUrl, html) {
const match = html.match(/data-sitekey="([^"]+)"/);
if (!match) return null;
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: new URLSearchParams({
key: this.apiKey,
method: 'userrecaptcha',
googlekey: match[1],
pageurl: pageUrl,
json: '1'
})
});
const { request: taskId } = await submitResp.json();
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const result = await fetch(
`https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const data = await result.json();
if (data.status === 1) return data.request;
}
throw new Error('reCAPTCHA solve timed out');
}
generateReport() {
const passed = this.results.filter(r => r.passed).length;
const failed = this.results.filter(r => !r.passed).length;
return {
total: this.results.length,
passed,
failed,
passRate: `${((passed / this.results.length) * 100).toFixed(1)}%`,
details: this.results
};
}
}
// Usage
const suite = new RegistrationTestSuite('YOUR_API_KEY', 'https://register.university.edu');
const report = await suite.runAll({ studentId: 'test123', password: 'testpass' });
console.log(`Tests: ${report.passed}/${report.total} passed (${report.passRate})`);
Welche Testszenarien die meisten Abfragen erzeugen
| Testszenario | Häufigkeit der Abfragen | Auswirkung auf den Lauf |
|---|---|---|
| Login mit mehreren Testkonten | hoch | jeder Anmeldeversuch erzeugt eine Abfrage |
| parallele Belegungstests | hoch | der Anti-Bot-Schutz reagiert auf Gleichzeitigkeit |
| schnelle Kurssuchen hintereinander | mittel | Rate-Limiting greift |
| Stundenplan erzeugen | niedrig | läuft innerhalb einer Sitzung |
| Prüfung der Voraussetzungen | niedrig | Teil des Belegungsvorgangs |
Threads planen: Rechenbeispiel Anmeldewoche
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – jeder Tarif enthält unbegrenzte Lösungen pro Thread. Für die Planung zählt deshalb nur, wie viele Abfragen gleichzeitig offen sind, nicht wie viele im Monat zusammenkommen.
Ein typisches Szenario aus der Anmeldewoche zum Wintersemester: Eine Regressionssuite mit 40 Testfällen läuft nachts durch, 25 davon berühren eine geschützte Seite. Bei fünf parallelen Test-Workern sind höchstens fünf Abfragen gleichzeitig in Arbeit – BASIC (15 $/Monat, 5 Threads) genügt dafür. Wird daraus ein Lasttest mit 50 simulierten Studierenden, die zeitgleich belegen, brauchen Sie mehr Parallelität: ADVANCE (90 $/Monat, 50 Threads) deckt genau diesen Fall ab. Dazwischen liegt STANDARD (30 $/Monat, 15 Threads) für Teams, die tagsüber mehrere Suites nebeneinander fahren. Alle Preise verstehen sich in US-Dollar.
Typische Fehlerbilder und ihre Ursachen
| Beobachtung | Ursache | Vorgehen |
|---|---|---|
| Bei jedem Testfall erscheint eine neue Abfrage | die Sitzung wird nicht wiederverwendet | einmal anmelden und alle Schritte in derselben Session ausführen |
| Belegung schlägt trotz gültigem Token fehl | das CSRF-Token ist inzwischen abgelaufen | CSRF-Token unmittelbar vor dem Absenden neu auslesen |
Meldung reCAPTCHA site key not found |
das Portal rendert die Abfrage per JavaScript nach | Sitekey aus dem gerenderten DOM holen, etwa über Selenium oder Playwright |
| Testdaten sind beim nächsten Lauf verschwunden | die Staging-Instanz setzt Belegungen zurück | Vorbedingungen zu Beginn jedes Laufs neu anlegen |
| Der Thread-Verbrauch steigt unerwartet | zu viele Wiederholungen und unnötige Seitenaufrufe | nur an geschützten Schritten lösen und Wiederholungen pro Testfall protokollieren |
Häufige Fragen
Welche CAPTCHA-Typen kommen in Campus-Portalen vor?
Fast immer reCAPTCHA v2, an interaktiven Werkzeugen gelegentlich Cloudflare Turnstile. Beide Typen löst CaptchaAI: reCAPTCHA v2 in der Regel in unter 60 Sekunden, Turnstile in unter 10 Sekunden.
Wie viele Threads braucht eine nächtliche Testsuite?
So viele, wie Testfälle gleichzeitig laufen. Fünf parallele Worker brauchen fünf Threads, also BASIC (15 $/Monat, 5 Threads). Erst Lasttests mit vielen simulierten Sitzungen machen ADVANCE (90 $/Monat, 50 Threads) nötig.
Kostet jede gelöste Abfrage extra?
Nein. Abgerechnet wird pro Thread und Monat, die Zahl der Lösungen pro Thread ist nicht begrenzt. Ein Testlauf mit 500 Abfragen kostet damit dasselbe wie einer mit 50.
Dürfen wir mit echten Studierendendaten testen?
Besser nicht. Matrikelnummern und Prüfungsdaten sind personenbezogen; für Tests genügen synthetische Konten in der Staging-Umgebung. Klären Sie den Zugriff vorab mit dem Rechenzentrum und dem Datenschutzbeauftragten.
Was tun, wenn das Token beim Absenden bereits abgelaufen ist?
Lösen Sie die Abfrage erst unmittelbar vor dem Absenden des Formulars. Tokens sind nur kurz gültig (rund 120 Sekunden) – wer sie am Anfang eines langen Testfalls holt, sendet sie am Ende zu spät ab.
Verwandte Leitfäden
- Login-CAPTCHAs im Test automatisch verarbeiten
- reCAPTCHA v2 über die API lösen
- Schnellstart mit CaptchaAI
Nächste Schritte
Richten Sie das Lösen zuerst für den Login-Testfall ein und erweitern Sie es danach auf Belegung und Warteliste. Mit einem API-Schlüssel und wenigen Zeilen Code läuft die Suite ohne manuelles Eingreifen durch.