大学课程注册系统使用 reCAPTCHA v2 来保护注册表、课程搜索页面和日程规划器。测试这些系统的 QA 团队需要跨多个测试场景处理验证码 - 注册工作流程、候补名单逻辑、席位可用性和截止日期执行。以下是如何将验证码解决集成到注册测试中。
验证码出现在注册系统中的位置
| 注册步骤 | 验证码类型 | 目的 |
|---|---|---|
| 学生登录页面 | reCAPTCHA v2 | 防止撞库 |
| 课程搜索/browse | reCAPTCHA v2 | 限制自动查找 |
| 添加/drop课程 | reCAPTCHA v2 | 防止自动注册 |
| 候补登记 | reCAPTCHA v2 | 阻止自动注册机器人 |
| 日程规划师 | Cloudflare Turnstile | 保护交互工具 |
| 成绩单请求 | reCAPTCHA v2 | 验证人类用户 |
使用 CAPTCHA 解决方案进行注册测试
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
测试套件运行程序 (JavaScript)
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})`);
触发验证码的测试场景
| 测试场景 | 验证码频率 | 影响 |
|---|---|---|
| 使用多个帐户登录 | 高的 | 每次登录尝试都会触发验证码 |
| 并发入学测试 | 高的 | 反机器人保护 |
| 快速课程搜索 | 缓和 | 限速查询 |
| 时间表生成 | 低的 | 单会话交互 |
| 先决条件验证 | 低的 | 部分招生流程 |
故障排除
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 每次测试登录时都使用验证码 | 没有会话重用 | 验证一次,在会话中运行测试 |
| 验证码后注册失败 | CSRF 令牌已过期 | 使用 CAPTCHA 提取新的 CSRF 令牌 |
| 测试数据在运行之间重置 | 门户清除考试报名情况 | 使用专用测试环境 |
| 找不到 reCAPTCHA 站点密钥 | SPA 动态渲染验证码 | 使用浏览器自动化来访问渲染的 DOM |
常问问题
我应该使用没有验证码的测试环境吗?
理想情况下,暂存环境禁用验证码。当这不可能时(或测试生产行为时),CaptchaAI 会处理 reCAPTCHA v2 求解,因此测试无需手动干预即可运行。
如何处理 CI/CD 测试管道中的验证码?
添加 CaptchaAI 作为测试依赖项。当测试遇到验证码时,通过 API 解决它并继续。这可以在不禁用安全功能的情况下保持端到端测试的可靠性。
我可以使用验证码在负载下测试注册吗?
是的。 CaptchaAI 处理负载测试的并行 reCAPTCHA 求解。为每个模拟学生会话同时提交多个求解请求。
相关文章
下一步
端到端测试注册系统 –获取您的 CaptchaAI API 密钥并在您的测试套件中处理大学门户验证码。