Use Cases

持续集成测试中的验证码处理

CI 流水线跑到一半,测试全线标红——往往不是代码错了,而是页面弹出验证码,无头 Chrome 点不了。验证码这一步必须交给 API 自动处理,端到端测试才能在 GitHub Actions、GitLab CI、Jenkins 里稳定跑完。


验证码为什么会拖垮 CI

CI/CD 是全自动的,验证码却是给人看的。没有识别服务,测试套件每次撞上验证码都会失败,还容易和真正的业务 bug 混在一起。常见的几种翻车场景:

  • reCAPTCHA 弹出后脚本卡死,直到 timeout 才失败
  • Turnstile 没通过,表单返回 403,日志分不清是验证码问题还是接口改了
  • 本地能跑通、CI 里挂——本地有头浏览器人能点一下,CI 无头 Chrome 没人点

做法: 在测试代码里接入 CaptchaAI 的 API,把 API Key 存成 CI 机密变量。遇到验证码自动提交、轮询、拿 token,不需要人工介入。


整体架构

流程如下:

┌──────────────┐     ┌──────────────┐     ┌────────────┐     ┌──────────────┐
│ Git Push     │────▶│ CI Runner    │────▶│ E2E Tests  │────▶│ Test Report  │
│              │     │ (headless    │     │ + CAPTCHA  │     │              │
│              │     │  Chrome)     │     │ solving    │     │              │
└──────────────┘     └──────────────┘     └────────────┘     └──────────────┘
                                                │
                                                ▼
                                         ┌────────────┐
                                         │ CaptchaAI  │
                                         │ API        │
                                         └────────────┘

四个阶段全程无人工介入:

  1. Git Push 触发 CI Runner
  2. Runner 拉起无头 Chrome 跑端到端测试
  3. 测试脚本内嵌的 CaptchaAI 调用在遇到验证码时拿 token
  4. 测完直接出 Test Report

国内团队接入:Gitee Go 与自建 Runner

CaptchaAI 是 HTTP API,不挑平台,接入方式和下文一致,换成对应平台的机密变量即可:

  • Runner 出口网络能访问 ocr.captchaai.com
  • pip 慢就加镜像:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple selenium requests pytest

封装一个 CI 专用的 Helper 类

把提交、轮询、超时都封装进一个类,用例不用重复写:

import os
import time
import requests


class CICaptchaSolver:
    """CAPTCHA solver designed for CI environments."""
    BASE = "https://ocr.captchaai.com"

    def __init__(self):
        self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
        if not self.api_key:
            raise EnvironmentError("CAPTCHAAI_API_KEY not set")

    def solve(self, params, initial_wait=10, timeout=120):
        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(f"CAPTCHA submit failed: {resp['request']}")

        task_id = resp["request"]
        time.sleep(initial_wait)
        deadline = time.time() + timeout

        while time.time() < deadline:
            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(f"CAPTCHA solve failed: {result['request']}")

        raise TimeoutError("CAPTCHA solve timed out in CI")

    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,
        })

接入 pytest:fixture 与测试用例

conftest.py:共享 fixture

captcha_solverbrowser 两个 fixture 放进 conftest.py,用例直接引用:

import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


@pytest.fixture(scope="session")
def captcha_solver():
    return CICaptchaSolver()


@pytest.fixture(scope="function")
def browser():
    options = Options()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-gpu")
    driver = webdriver.Chrome(options=options)
    driver.set_window_size(1920, 1080)
    yield driver
    driver.quit()

测试用例:登录页和联系表单两个场景

分别覆盖 reCAPTCHA v2 登录页和 Turnstile 联系表单,拿到 token 后用 execute_script 写入隐藏字段再提交:

import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class TestLoginFlow:
    SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
    LOGIN_URL = "https://staging.example.com/qa-login"

    def test_login_with_captcha(self, browser, captcha_solver):
        browser.get(self.LOGIN_URL)

        # Fill credentials
        browser.find_element(By.ID, "username").send_keys("testuser")
        browser.find_element(By.ID, "password").send_keys("testpass123")

        # Solve CAPTCHA
        token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
        browser.execute_script(
            f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
        )

        # Submit
        browser.find_element(By.ID, "login-btn").click()
        time.sleep(3)

        # Verify login success
        assert "dashboard" in browser.current_url.lower()

    def test_login_wrong_password(self, browser, captcha_solver):
        browser.get(self.LOGIN_URL)
        browser.find_element(By.ID, "username").send_keys("testuser")
        browser.find_element(By.ID, "password").send_keys("wrongpass")

        token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
        browser.execute_script(
            f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
        )

        browser.find_element(By.ID, "login-btn").click()
        time.sleep(3)

        error = browser.find_element(By.CSS_SELECTOR, ".error-message")
        assert error.is_displayed()


class TestContactForm:
    SITEKEY = "0x4AAAA..."
    FORM_URL = "https://staging.example.com/contact"

    def test_contact_form_submission(self, browser, captcha_solver):
        browser.get(self.FORM_URL)

        browser.find_element(By.ID, "name").send_keys("CI Test")
        browser.find_element(By.ID, "email").send_keys("[email protected]")
        browser.find_element(By.ID, "message").send_keys("Automated CI test")

        token = captcha_solver.solve_turnstile(self.SITEKEY, self.FORM_URL)
        browser.execute_script(
            f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
        )

        browser.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        WebDriverWait(browser, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
        )

三段配置任选其一:

CI 平台 常见场景
GitHub Actions 出海团队、GitHub 托管仓库
GitLab CI 自建 / 私有化部署
Jenkins 已有 Jenkins 基础设施

GitHub Actions 工作流程配置

CAPTCHAAI_API_KEY 存进仓库 Secrets,运行时按环境变量注入即可。确认:

  • Secrets 里已加好 CAPTCHAAI_API_KEY
  • Runner 用 ubuntu-latest
name: E2E Tests with CAPTCHA

on:
  push:
    branches: [main, staging]
  pull_request:
    branches: [main]

jobs:
  e2e-tests:
    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Chrome
        uses: browser-actions/setup-chrome@v1
        with:
          chrome-version: stable

      - name: Install ChromeDriver
        uses: nanasess/setup-chromedriver@v2

      - name: Install dependencies
        run: |
          pip install selenium requests pytest pytest-html

      - name: Run E2E tests
        env:
          CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
        run: |
          pytest tests/e2e/ -v --html=report.html --self-contained-html

      - name: Upload test report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: e2e-report
          path: report.html

GitLab CI 配置

selenium/standalone-chrome 作为 service 容器,Key 走 CI/CD Variables,需勾选 masked。确认:

  • CAPTCHAAI_API_KEY 勾了 Masked,不会在日志里明文出现
  • Runner 支持 Docker executor
e2e_tests:
  stage: test
  image: python:3.11
  services:

    - selenium/standalone-chrome:latest
  variables:
    SELENIUM_REMOTE_URL: "http://selenium__standalone-chrome:4444/wd/hub"
  script:

    - pip install selenium requests pytest
    - pytest tests/e2e/ -v
  artifacts:
    when: always
    reports:
      junit: report.xml

Jenkins 流水线配置

credentials() 绑定 Key,比明文写进环境变量更安全。确认:

  • 已装 Credentials Binding 插件,凭据类型选 Secret text
  • 凭据 ID 和 pipeline 里的 captchaai-api-key 对得上
pipeline {
    agent any
    environment {
        CAPTCHAAI_API_KEY = credentials('captchaai-api-key')
    }
    stages {
        stage('Setup') {
            steps {
                sh 'pip install selenium requests pytest'
            }
        }
        stage('E2E Tests') {
            steps {
                sh 'pytest tests/e2e/ -v --junitxml=results.xml'
            }
        }
    }
    post {
        always {
            junit 'results.xml'
        }
    }
}

控制 CI 里的验证码成本

按需触发,而不是每次构建都测

每个 PR 都跑验证码测试,账单涨得快。用开关控制何时真正执行:

import os

def should_run_captcha_tests():
    """Skip CAPTCHA tests in certain environments."""
    if os.environ.get("SKIP_CAPTCHA_TESTS"):
        return False
    if not os.environ.get("CAPTCHAAI_API_KEY"):
        return False
    return True


# In test
import pytest

@pytest.mark.skipif(
    not should_run_captcha_tests(),
    reason="CAPTCHA tests disabled or API key not set"
)
class TestWithCaptcha:
    def test_login(self, browser, captcha_solver):
        pass

跑测试前先查一下余额

余额不够就提前跳过,比测到一半频繁失败省事:

@pytest.fixture(scope="session", autouse=True)
def check_captcha_balance(captcha_solver):
    import requests
    resp = requests.get(
        f"{captcha_solver.BASE}/res.php",
        params={"key": captcha_solver.api_key, "action": "getbalance"},
    )
    balance = float(resp.text)
    if balance < 0.50:
        pytest.skip(f"CaptchaAI balance too low: ${balance:.2f}")

故障排除

问题 原因 处理方式
CAPTCHAAI_API_KEY not set 密钥没配置 加到 CI 机密变量
Chrome 在 CI 里崩溃 缺少 --no-sandbox 参数 补上启动参数
本地能跑通,CI 里失败 浏览器版本不一致 锁定 Chrome 版本
验证码识别超时 CI 出口网络较慢 调大 timeout
测试跑一次很贵 触发次数太多 PR 构建加 SKIP_CAPTCHA_TESTS

常见问题

多久跑一次验证码测试比较合适?

不用每次 push 都跑。合并主分支或每天定时跑一次即可,PR 阶段用 SKIP_CAPTCHA_TESTS 跳过,省 API 调用和 CI 时间。

API Key 在 CI 里应该怎么存?

交给平台自带的机密管理:GitHub Secrets、GitLab CI/CD Variables、Jenkins Credentials,都支持加密存储。别把 Key 写进代码或 YAML 明文。

验证码识别超时了,要不要在流水线里加重试?

先把 timeout 调大——CI Runner 的网络通常比本地慢,很多“超时”只是等待不够。真正失败了再重试一次即可,别无限重试拖长流水线。

国内团队用 Gitee Go 或者自建 GitLab Runner,也能这样接吗?

可以。CaptchaAI 是 HTTP API,只要 Runner 能访问 ocr.captchaai.com 即可,平台不限。

多个仓库能不能共用同一个 API Key?

可以,Key 不与仓库数量绑定。每个仓库仍要把 Key 存成自己的机密变量,别把明文提交进代码库或打进日志。


相关指南


把验证码识别加进你的 CI 流水线——立即注册 CaptchaAI

该文章已禁用评论。