注册页几乎清一色挂着验证码——reCAPTCHA、Turnstile,或是国内常见的 GeeTest 滑块。测试脚本一走到这一步就卡住,团队要么手动介入,要么干脆跳过这条用例。
这篇指南给出一套可复用的注册流程测试框架:用 Python、Selenium 加 CaptchaAI API 自动识别验证码,覆盖正常注册、重复邮箱、弱密码等边缘场景,并接入 pytest。
为什么注册测试离不开验证码识别
注册表单挂验证码是为了拦截批量建号的机器人,却也把自动化测试一起挡在了门外。一套成熟的注册流程测试,通常需要做到:
- 确认注册链路在 staging 和生产环境都能跑通
- 覆盖边缘用例:重复邮箱、弱密码、必填字段缺失
- 每次发布后自动跑一轮回归测试,而不是靠人工点一遍
- 验证验证码组件没有把表单弄坏,比如遮挡提交按钮
出海团队往往要覆盖两套场景:面向海外用户多半是 reCAPTCHA v2 或 Cloudflare Turnstile,面向国内用户则更常见 GeeTest(极验)滑块。下面的代码聚焦前两种;换成 GeeTest v3 也是同样思路,只要把 _solve_captcha() 的判断逻辑换成对应接口即可。
整体测试架构
整个流程拆成四步:造数据、填表单、过验证码、验证结果。
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
每一步都能独立测试和替换,换验证码类型或换断言逻辑都不需要动前面的步骤。
代码实现
下面是核心代码:生成测试数据、封装验证码识别逻辑、执行测试用例。
生成随机测试用户数据
每次跑测试都需要一个全新的账号,避免用例之间互相污染:
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,
}
封装验证码识别与注册测试逻辑
CaptchaSolver 负责调用 CaptchaAI API 识别验证码,RegistrationTester 负责驱动浏览器、跑测试用例、记录结果:
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()
_solve_captcha() 先看页面有没有 Turnstile 的 .cf-turnstile 元素,命中就调用 solve_turnstile();否则再看是否带 data-sitekey 且含 reCAPTCHA 特征,命中就调用 solve_recaptcha()——两者互斥,不会重复提交。识别到的 token 分别写进 cf-turnstile-response 和 g-recaptcha-response 字段,再点提交。三个用例——valid_registration、duplicate_email、weak_password——分别验证正常注册、重复邮箱报错、弱密码校验,结果统一记录进 self.results。
快速跑起来
把 YOUR_API_KEY 换成你自己的 API Key,指向 staging 环境的注册页就能跑:
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
跑完之后会看到类似这样的结果:
输出:
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
接入 pytest
如果项目本来就用 pytest 管理测试,把上面这套逻辑包成一个 module 级 fixture,三个用例就能直接复用同一个浏览器会话:
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"
常见问题
验证码没有被识别到,要怎么排查?
先确认验证码组件是不是延迟加载——不少站点会等表单渲染完才插入 reCAPTCHA 或 Turnstile。在 _solve_captcha() 前加一段短暂等待即可。
可以在没有获得授权的网站上跑这套自动注册测试吗?
不可以。这套框架只适用于你自己拥有或已获得授权的应用,不要拿去对第三方网站做批量注册测试。
这套测试能接入 CI/CD 吗?
可以。把 Chrome 换成无头模式(options.add_argument("--headless")),API Key 放进 CI 环境变量即可。记得给识别步骤留够超时时间,CI 网络通常比本地慢。
国内电商注册页用的是 GeeTest 滑块,这套代码要怎么改?
把 _solve_captcha() 的判断分支换成 GeeTest v3 的检测逻辑(找 gt、challenge 参数),其余生成数据、跑用例的部分不用动。
常见报错与排查
大多数坑都出在时机和选择器上:
- 注册明明成功,测试却报失败——成功判断的关键词没匹配上,把你网站实际的成功提示文案加进
_check_success()。 - 脚本没检测到验证码——验证码组件延迟加载,在调用
_solve_captcha()前加一行time.sleep(2)。 - 找不到表单字段——页面 HTML 结构和示例不一样,按你网站实际的 CSS 选择器改一下。
- token 已过期——识别完成到提交隔太久,把
_solve_captcha()挪到紧挨提交按钮的位置。
延伸阅读
提交环节的验证码处理另见 自动表单提交中的验证码处理;接口参数速查见 CaptchaAI API 速查手册。
把验证码从注册流程测试的阻塞点里挪开——免费试用 CaptchaAI。