Tutorials

使用 CaptchaAI 构建自动化测试管道

端到端测试为什么总卡在验证码这一步?答案是把验证码识别做成测试基础设施的一部分,让 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 开始

该文章已禁用评论。