Espresso 测试卡在 WebView 里的验证码上,本质上是缺一座桥:拿到 sitekey,交给识别接口,把 token 写回页面。
CaptchaAI 补上这座桥,下面这套方案能直接搬进项目里用,分三步:
- 用仅在 debug 环境生效的
JavascriptInterface探测 WebView 里的验证码 - 把 sitekey、pageurl 交给 CaptchaAI API 完成识别
- 把 token 注入 WebView,让 Espresso 用例接着跑完登录、注册或支付流程
什么时候会卡在验证码上
跨境电商类 App 经常会在 WebView 里内嵌第三方支付页面,页面在放行付款前弹出 reCAPTCHA v2。
这是很多做海外业务的国内团队都会遇到的场景。
Espresso 的插桩测试一旦跑到这一步就会卡住:没有人手动点验证码,测试直接超时失败。
本文用到的环境:Android Studio、Kotlin、Espresso、AndroidX Test、CaptchaAI API、Python。
第一步:在 App 里加一个调试专用的验证码探测助手
在 debug source set 里加一个 helper。
它能对 WebView 执行 JavaScript,拿到 sitekey 和当前页面地址:
// CaptchaTestHelper.kt — debug source set only
package com.example.app.testing
import android.webkit.JavascriptInterface
import android.webkit.WebView
import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
class CaptchaTestHelper(private val webView: WebView) {
private var detectedSitekey: String? = null
private var detectedPageUrl: String? = null
private var solvedToken: String? = null
@JavascriptInterface
fun onCaptchaDetected(sitekey: String, pageurl: String) {
detectedSitekey = sitekey
detectedPageUrl = pageurl
}
fun detectCaptcha() {
webView.post {
webView.evaluateJavascript("""
(function() {
var el = document.querySelector('.g-recaptcha');
if (el) {
CaptchaHelper.onCaptchaDetected(
el.getAttribute('data-sitekey'),
window.location.href
);
return 'found';
}
return 'not_found';
})();
""", null)
}
}
suspend fun solveAndInject(): Boolean = withContext(Dispatchers.IO) {
val sitekey = detectedSitekey ?: return@withContext false
val pageurl = detectedPageUrl ?: return@withContext false
// Call backend solver
val client = OkHttpClient.Builder()
.callTimeout(java.time.Duration.ofMinutes(3))
.build()
val body = JSONObject().apply {
put("captchaType", "recaptcha_v2")
put("sitekey", sitekey)
put("pageurl", pageurl)
}.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("http://10.0.2.2:3000/api/solve-captcha") // Host loopback for emulator
.post(body)
.build()
val response = client.newCall(request).execute()
val json = JSONObject(response.body?.string() ?: "")
val token = json.optString("token", "")
if (token.isEmpty()) return@withContext false
solvedToken = token
// Inject token on main thread
withContext(Dispatchers.Main) {
webView.evaluateJavascript("""
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) {}
""", null)
}
return@withContext true
}
companion object {
fun attach(webView: WebView): CaptchaTestHelper {
val helper = CaptchaTestHelper(webView)
webView.addJavascriptInterface(helper, "CaptchaHelper")
return helper
}
}
}
第二步:用 Python 写一个后端求解器
测试运行期间,在开发机上跑这个 Flask 服务。
它负责把 sitekey 和 pageurl 提交给 CaptchaAI,再轮询拿到 token:
# android_test_solver.py
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
# Submit to CaptchaAI
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": data["sitekey"],
"pageurl": data["pageurl"],
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
return jsonify({"error": result.get("request")}), 400
task_id = result["request"]
# Poll for result
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)
第三步:在 Espresso 用例里接入验证码处理逻辑
把探测、识别、注入这三步,串进真实的结账验证测试:
// CheckoutCaptchaTest.kt
package com.example.app
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CheckoutCaptchaTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testCheckoutWithCaptcha() {
// Navigate to checkout
onView(withId(R.id.checkout_button)).perform(click())
// Wait for WebView to load
Thread.sleep(5000)
// Access the WebView and attach helper
activityRule.scenario.onActivity { activity ->
val webView = activity.findViewById<android.webkit.WebView>(R.id.webview)
val helper = CaptchaTestHelper.attach(webView)
helper.detectCaptcha()
// Wait for detection
Thread.sleep(2000)
// Solve and inject
runBlocking {
val solved = helper.solveAndInject()
assert(solved) { "CAPTCHA should be solved successfully" }
}
}
// Continue with form submission after token 提交
Thread.sleep(1000)
// Verify checkout completed
onView(withText("Order Confirmed")).check(
androidx.test.espresso.assertion.ViewAssertions.matches(isDisplayed())
)
}
}
常见报错与排查
| 问题 | 原因 | 处理方式 |
|---|---|---|
10.0.2.2 连不上 |
没有在 Android 模拟器里跑 | 真机测试改用宿主机实际 IP |
evaluateJavascript 回调是空的 |
WebView 还没加载完 | 在执行脚本前,先加一个 WebViewClient.onPageFinished() 监听 |
addJavascriptInterface 没反应 |
JavaScript 被禁用了 | 调用 webView.settings.javaScriptEnabled = true |
| 网络请求被明文策略拦截 | Android 9 及以上访问 HTTP 本地地址 | 在 AndroidManifest.xml 里加 android:usesCleartextTraffic="true"(仅 debug 用) |
表格没解决问题时,先确认:
- Flask 有没有在跑
- 模拟器网络是否互通
API_KEY是否读取正确
常见问题
Espresso 能不能直接操作 WebView 里的验证码?
不能,onWebView() 只支持基础交互,验证码处理要用 evaluateJavascript()。
国内环境跑这套测试,会不会因为连不上 Google 服务而卡住?
reCAPTCHA 脚本走 Google 域名,加载可能不稳定,但识别请求直接打给 CaptchaAI,不受影响,放宽两步超时即可。
CaptchaTestHelper 会不会混进正式包?
不会,放进 src/debug/java/ source set 即可,Android 构建变体会自动从 release 包排除。
多台模拟器并行跑测试,需要开更多 CaptchaAI 线程吗?
会有影响,CaptchaAI 按线程计费,BASIC $15/月含 5 线程;并行数上去了就对齐线程数。
WebView 里出现的是极验(GeeTest)而不是 reCAPTCHA,这套方案还能用吗?
思路一样:selector 换成对应 DOM 结构,method 参数换成 CaptchaAI 支持的类型。
GeeTest 目前支持到 v3,注入方式按回调机制调整即可。
下一步
想落地?获取 CaptchaAI API Key,部署好本文的求解器就能直接用。
相关指南:
相关文章
- 如何用 API 解决 reCAPTCHA v2 Callback 验证码
- 用 CaptchaAI 搭建自动化测试流水线
- reCAPTCHA v2 与 Turnstile 同站点处理方案