端到端测试为什么总卡在验证码这一步?答案是把验证码识别做成测试基础设施的一部分,让 CI 自动识别、自动继续。下面用 pytest、Selenium 和 CaptchaAI 搭建可复用的测试管道。很多团队干脆跳过带验证码的登录页、结账页,核心链路反而成了测试盲区;把识别逻辑封装成独立的 helper 之后,测试用例本身不用关心验证码细节,用例数量增加也不会带来额外的维护成本。
项目目录结构
把验证码逻辑放进 helpers/,方便多个测试文件复用;国内环境可用 pip 镜像源加速依赖安装。conftest.py 里的 fixture 按 session 和 function 分开作用域——浏览器实例每个用例都重建,CaptchaAI 客户端整个测试会话只初始化一次,能省下不少重复握手的开销:
tests/
├── conftest.py # Shared fixtures
├── helpers/
│ ├── captcha.py # CaptchaAI integration
│ └── browser.py # Selenium helpers
├── test_login.py # Login flow tests
├── test_checkout.py # Checkout flow tests
└── pytest.ini # Config
封装 CaptchaAI 验证码测试工具类
CaptchaTestHelper 把提交、轮询、把 token 写入浏览器页面这三步封装成一个类,测试代码只需调用 solve_recaptcha():
# tests/helpers/captcha.py
import requests
import time
import os
class CaptchaTestHelper:
"""Solve CAPTCHAs during automated tests."""
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY required for CAPTCHA tests")
def solve_recaptcha(self, sitekey, pageurl):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
}, timeout=30)
result = resp.json()
if result.get("status") != 1:
raise RuntimeError(f"Submit failed: {result.get('request')}")
task_id = result["request"]
time.sleep(15)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
return data["request"]
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout in test")
def inject_token(self, driver, token):
"""Inject solved token into Selenium browser."""
driver.execute_script(
'document.getElementById("g-recaptcha-response").value = arguments[0];',
token,
)
# Trigger callback if available
driver.execute_script("""
if (typeof ___grecaptcha_cfg !== 'undefined') {
var clients = ___grecaptcha_cfg.clients;
for (var key in clients) {
var client = clients[key];
for (var prop in client) {
var val = client[prop];
if (val && typeof val === 'object') {
for (var inner in val) {
if (typeof val[inner] === 'function') {
val[inner](arguments[0]);
return;
}
}
}
}
}
}
""", token)
inject_token 还会触发 reCAPTCHA 回调,漏掉会卡在提交按钮;单元测试可直接 mock solve_recaptcha。轮询间隔固定为 5 秒、最多重试 24 次,超过上限直接抛出 TimeoutError,pytest 会把它当作用例失败处理,不会拖住整条流水线。
Pytest Fixture 配置
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from helpers.captcha import CaptchaTestHelper
@pytest.fixture(scope="session")
def captcha_solver():
return CaptchaTestHelper()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope="session")
def base_url():
return "https://staging.example.com"
登录流程测试:识别验证码后提交表单
覆盖两条路径:验证码正确应登录成功,账密错误仍应正常报错:
# tests/test_login.py
import pytest
from selenium.webdriver.common.by import By
class TestLogin:
def test_valid_login_with_captcha(self, browser, captcha_solver, base_url):
"""Test that login succeeds when CAPTCHA is solved correctly."""
browser.get(f"{base_url}/login")
# Fill form
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "password").send_keys("testpassword123")
# Solve CAPTCHA
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit
browser.find_element(By.ID, "login-btn").click()
# Assert redirect to dashboard
assert "/dashboard" in browser.current_url
assert browser.find_element(By.CLASS_NAME, "welcome-message")
def test_invalid_credentials_with_captcha(self, browser, captcha_solver, base_url):
"""Test that wrong credentials show error even with valid CAPTCHA."""
browser.get(f"{base_url}/login")
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "password").send_keys("wrongpass")
sitekey = browser.find_element(
By.CLASS_NAME, "g-recaptcha"
).get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
browser.find_element(By.ID, "login-btn").click()
error = browser.find_element(By.CLASS_NAME, "error-message")
assert "Invalid" in error.text
结账流程测试:多步骤表单里的验证码
结账比登录复杂,选择器一变就可能断链,建议单独跑:
# tests/test_checkout.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestCheckout:
def test_checkout_flow_with_captcha(self, browser, captcha_solver, base_url):
"""Full checkout flow: add item, fill form, solve CAPTCHA, confirm."""
# Add item to cart
browser.get(f"{base_url}/products/test-item")
browser.find_element(By.ID, "add-to-cart").click()
# Go to checkout
browser.get(f"{base_url}/checkout")
# Fill shipping
browser.find_element(By.ID, "address").send_keys("123 Test St")
browser.find_element(By.ID, "city").send_keys("Test City")
browser.find_element(By.ID, "zip").send_keys("12345")
# Solve CAPTCHA on checkout page
captcha_el = browser.find_element(By.CLASS_NAME, "g-recaptcha")
sitekey = captcha_el.get_attribute("data-sitekey")
token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
captcha_solver.inject_token(browser, token)
# Submit order
browser.find_element(By.ID, "place-order").click()
# Wait for confirmation
wait = WebDriverWait(browser, 15)
confirmation = wait.until(
EC.presence_of_element_located((By.CLASS_NAME, "order-confirmation"))
)
assert "Thank you" in confirmation.text
Pytest 配置文件
用 captcha 标记单独分组,配合 CI 控制何时真正调用 CaptchaAI:
# tests/pytest.ini
[pytest]
markers =
captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short
GitHub Actions 自动化测试工作流
push 到 main 分支或每周一 6 点(UTC)触发一次;CAPTCHAAI_API_KEY 放进 GitHub Secrets,不要写进工作流文件:
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6 AM
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pytest selenium requests
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: pytest tests/ -m captcha -v
若目标站点用 reCAPTCHA,GitHub 托管 runner 通常比境内自建 runner 更容易稳定加载 Google 脚本,境内 runner 偶尔因此出现假失败。
故障排除
排查顺序建议先看 CI 日志里 CaptchaAI 的返回状态码,再确认 Chrome 版本和网络延迟是否正常,最后才怀疑页面选择器变化——大多数假失败都出在前两步:
| 问题 | 原因 | 处理方式 |
|---|---|---|
| token 写入失败 | 找不到文本域 | 检查元素 ID 或用 querySelector('[name="g-recaptcha-response"]') |
| 本地通过,CI 失败 | Chrome 版本不一致 | 固定 CI 里的 Chrome 版本 |
| staging 没出现验证码 | staging 关掉了验证码 | 手动开启验证码 |
| 一直等不到结果 | 网络慢或超时太短 | 轮询超时调到 180 秒以上 |
常见问题
运行验证码相关的自动化测试贵吗?
单次识别几美分,10 个用例每天跑一次,月成本一般在 $10 以内。
CAPTCHAAI_API_KEY 在 GitHub Actions 里怎么存?
仓库 Settings → Secrets and variables → Actions 新建 secret,工作流里用 ${{ secrets.CAPTCHAAI_API_KEY }} 引用,不要写进 YAML。
除了 reCAPTCHA v2,还能测 Turnstile 或 GeeTest 吗?
可以,把 solve_recaptcha 换成对应提交逻辑,三步不变;CaptchaAI 支持 GeeTest v3,v4 暂未开放。
验证码测试要不要每次 CI 都跑?
不建议,按次计费;建议只在合并 main 或定时任务里跑,PR 检查用 pytest -m "not captcha" 排除。
reCAPTCHA v2 识别通常要等多久?
SLA 上限是 60 秒内出结果,正常网络环境下更快;网络波动较大时,可以把轮询超时放宽到本文示例里的 180 秒,避免误判为失败。
相关指南
别让验证码拖慢你的测试进度——从 CaptchaAI 开始。