Use Cases

注册流程测试中的验证码处理

注册页几乎清一色挂着验证码——reCAPTCHA、Turnstile,或是国内常见的 GeeTest 滑块。测试脚本一走到这一步就卡住,团队要么手动介入,要么干脆跳过这条用例。

这篇指南给出一套可复用的注册流程测试框架:用 Python、Selenium 加 CaptchaAI API 自动识别验证码,覆盖正常注册、重复邮箱、弱密码等边缘场景,并接入 pytest。


为什么注册测试离不开验证码识别

注册表单挂验证码是为了拦截批量建号的机器人,却也把自动化测试一起挡在了门外。一套成熟的注册流程测试,通常需要做到:

  1. 确认注册链路在 staging 和生产环境都能跑通
  2. 覆盖边缘用例:重复邮箱、弱密码、必填字段缺失
  3. 每次发布后自动跑一轮回归测试,而不是靠人工点一遍
  4. 验证验证码组件没有把表单弄坏,比如遮挡提交按钮

出海团队往往要覆盖两套场景:面向海外用户多半是 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-responseg-recaptcha-response 字段,再点提交。三个用例——valid_registrationduplicate_emailweak_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 的检测逻辑(找 gtchallenge 参数),其余生成数据、跑用例的部分不用动。


常见报错与排查

大多数坑都出在时机和选择器上:

  1. 注册明明成功,测试却报失败——成功判断的关键词没匹配上,把你网站实际的成功提示文案加进 _check_success()
  2. 脚本没检测到验证码——验证码组件延迟加载,在调用 _solve_captcha() 前加一行 time.sleep(2)
  3. 找不到表单字段——页面 HTML 结构和示例不一样,按你网站实际的 CSS 选择器改一下。
  4. token 已过期——识别完成到提交隔太久,把 _solve_captcha() 挪到紧挨提交按钮的位置。

延伸阅读

提交环节的验证码处理另见 自动表单提交中的验证码处理;接口参数速查见 CaptchaAI API 速查手册


把验证码从注册流程测试的阻塞点里挪开——免费试用 CaptchaAI

该文章已禁用评论。