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 │
└────────────┘
四个阶段全程无人工介入:
- Git Push 触发 CI Runner
- Runner 拉起无头 Chrome 跑端到端测试
- 测试脚本内嵌的 CaptchaAI 调用在遇到验证码时拿 token
- 测完直接出 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_solver 和 browser 两个 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。