使用 XCUITest 进行 iOS 应用程序测试通常会遇到嵌入式 WKWebView 中的验证码 - 登录表单、支付网关和第三方集成都会带来 reCAPTCHA 挑战。CaptchaAI解决了这些问题,以便您的 UI 测试可以完成端到端流程,而无需手动干预。
本指南展示了如何在 XCUITest 运行期间检测 WKWebView 中的验证码,通过配套服务中的 CaptchaAI 解决它们,并将token 提交回 Web 内容中。
真实场景
您的 iOS 应用程序在 WKWebView 中加载注册表单。该表格包括reCAPTCHA v2。在自动化测试期间,此验证码会阻止测试进程。您需要一个解决方案:
- 在测试执行期间检测 WebView 中的 CAPTCHA
- 以编程方式提取站点密钥
- 通过CaptchaAI解决它
- 注入令牌以便表单可以提交
环境: Xcode 15+、Swift、XCUITest、macOS 测试运行程序、CaptchaAI API。
建筑学
XCUITest 无法直接在 WKWebView 中执行 JavaScript。该方法使用应用程序在测试期间调用的辅助端点:
| 成分 | 角色 |
|---|---|
| XCUI测试 | 驱动 UI,通过测试助手触发验证码解决 |
| 测试助手 API | 接收sitekey + URL,调用CaptchaAI,返回token |
| 应用程序测试挂钩 | 评估 WKWebView 中的 JavaScript 以检测/inject |
| CaptchaAI API | 解决验证码挑战 |
第 1 步:向应用程序添加测试挂钩
在应用程序的 WKWebView 控制器中,添加可以通过辅助功能标识符或 URL 方案触发的测试模式 CAPTCHA 处理程序:
// CaptchaTestHelper.swift — Add to app target (test build only)
import WebKit
#if DEBUG
class CaptchaTestHelper {
private let webView: WKWebView
init(webView: WKWebView) {
self.webView = webView
}
func detectCaptcha(completion: @escaping (String?, String?) -> Void) {
let script = """
(function() {
var el = document.querySelector('.g-recaptcha');
if (el) {
return JSON.stringify({
sitekey: el.getAttribute('data-sitekey'),
pageurl: window.location.href
});
}
return null;
})();
"""
webView.evaluateJavaScript(script) { result, error in
guard let jsonString = result as? String,
let data = jsonString.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
completion(nil, nil)
return
}
completion(json["sitekey"], json["pageurl"])
}
}
func injectToken(_ token: String, completion: @escaping (Bool) -> Void) {
let script = """
document.getElementById('g-recaptcha-response').value = '\(token)';
try {
var clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach(function(k) {
Object.keys(clients[k]).forEach(function(j) {
if (clients[k][j] && clients[k][j].callback) {
clients[k][j].callback('\(token)');
}
});
});
} catch(e) {}
true;
"""
webView.evaluateJavaScript(script) { _, error in
completion(error == nil)
}
}
func solveCaptchaViaBackend(
sitekey: String, pageurl: String,
completion: @escaping (Result<String, Error>) -> Void
) {
guard let url = URL(string: "http://localhost:3000/api/solve-captcha") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"captchaType": "recaptcha_v2",
"sitekey": sitekey,
"pageurl": pageurl
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let token = json["token"] as? String else {
completion(.failure(NSError(domain: "", code: -1,
userInfo: [NSLocalizedDescriptionKey: "No token"])))
return
}
completion(.success(token))
}.resume()
}
}
#endif
第2步:后端求解器服务
在测试期间运行与 CaptchaAI 通信的本地求解器服务:
# ios_test_solver.py — Run on test machine during XCUITest execution
import os
import time
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = os.environ.get("CAPTCHAAI_API_KEY", "YOUR_API_KEY")
@app.route("/api/solve-captcha", methods=["POST"])
def solve():
data = request.json
sitekey = data["sitekey"]
pageurl = data["pageurl"]
# Submit to CaptchaAI
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
return jsonify({"error": result.get("request")}), 400
task_id = result["request"]
# Poll
for _ in range(30):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
})
poll_result = poll.json()
if poll_result.get("status") == 1:
return jsonify({"token": poll_result["request"]})
if poll_result.get("request") != "CAPCHA_NOT_READY":
return jsonify({"error": poll_result["request"]}), 400
return jsonify({"error": "Timeout"}), 408
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000)
第 3 步:XCUITest 集成
在您的 XCUITest 中,当加载带有验证码的 WebView 时触发验证码解决流程:
// CaptchaUITests.swift
import XCTest
class CaptchaUITests: XCTestCase {
func testRegistrationWithCaptcha() throws {
let app = XCUIApplication()
app.launchArguments.append("--captcha-test-mode")
app.launch()
// Navigate to registration
app.buttons["Register"].tap()
// Wait for WebView to load
let webView = app.webViews.firstMatch
XCTAssertTrue(webView.waitForExistence(timeout: 15))
// Trigger CAPTCHA solve via test helper button
// (The app shows this button only in test mode)
let solveButton = app.buttons["SolveCaptchaTestHelper"]
if solveButton.waitForExistence(timeout: 5) {
solveButton.tap()
// Wait for solve completion indicator
let solved = app.staticTexts["CaptchaSolved"]
XCTAssertTrue(solved.waitForExistence(timeout: 120),
"CAPTCHA should be solved within 2 minutes")
}
// Continue with form submission
app.buttons["SubmitForm"].tap()
// Verify success
let success = app.staticTexts["Registration Complete"]
XCTAssertTrue(success.waitForExistence(timeout: 10))
}
}
过桥合同
- 定义测试运行程序发送到帮助程序服务的请求负载,包括目标 URL 和质询元数据。
- 返回带有令牌、过期和错误原因的结构化响应,以便测试层可以干净地分支。
- 保持模拟器日志和帮助服务日志通过共享跟踪标识符链接,以便更快地进行分类。
故障排除
| 问题 | 原因 | 处理方式 |
|---|---|---|
evaluateJavaScript 返回零 |
WebView 尚未完成加载 | 等待webView.isLoading == false后再注入JS |
| 无法从模拟器访问后端 | 本地主机无法访问 | 使用127.0.0.1或Mac的网络IP;检查应用程序传输安全 |
| token 提交不会触发回调 | reCAPTCHA 回调嵌套在复杂对象中 | 递归迭代 ___grecaptcha_cfg.clients 的所有属性 |
| XCUITest超时等待解决 | CaptchaAI 求解时间长 | 将验证码相关测试的测试超时设置为 120 秒以上 |
常问问题
XCUITest可以直接在WKWebView中执行JavaScript吗?
不可以。XCUITest 与 UI 元素交互,但无法评估 JavaScript。您需要在应用程序代码中使用测试挂钩(仅限调试构建)来弥补这一差距。
这种方法适用于 CI/CD 管道吗?
是的,在 CI 机器和 iOS 模拟器上运行求解器后端。求解器服务通过 HTTPS 与 CaptchaAI 进行通信,该服务可在任何环境中工作。
如何防止测试挂钩运送到生产环境?
将所有测试帮助程序代码包装在 #if DEBUG 编译器指令中。该代码将从发布版本中删除。
如果验证码位于第三方 SDK WebView 中怎么办?
如果您不控制 WebView,请使用 Appium - 它提供跨任何 WebView 上下文的 execute_script 功能,而无需应用程序端挂钩。
相关文章
- 如何使用Api解决Recaptcha V2回调
- Zapier 验证码无代码自动化
- Recaptcha V2 Turnstile同一站点处理
下一步
将 CaptchaAI 集成到您的 iOS 测试管道中 –”获取您的 API 密钥并通过验证码保护的流程实现自动化。
相关指南: