Les tests UI Android bases sur Espresso finissent souvent par tomber sur un CAPTCHA dans une WebView : page de connexion, tunnel d'inscription, checkout tiers ou parcours de paiement embarque. Tant que ce point n'est pas traite, le test E2E n'est plus vraiment automatisable. CaptchaAI permet d'ajouter une résolution programmatique au milieu de ce parcours sans faire intervenir un humain.
Le principe est simple : détecter le CAPTCHA dans la WebView, envoyer les bons paramètres à un petit backend de test, attendre le token puis le réinjecter dans la page pour poursuivre le scénario.
Scénario du monde réel
Votre application Android charge une page de paiement tierce dans une WebView. Cette page impose un reCAPTCHA v2 avant la validation finale. Pendant les tests instrumentés Espresso, ce challenge bloque le scénario de bout en bout et empêche de valider le flux de paiement.
Environnement : Android Studio, Kotlin, Espresso, AndroidX Test, CaptchaAI API, backend Python local.
Étape 1 : ajouter un helper de test dans l'application
Ajoutez un helper réservé au mode debug, capable d'exécuter du JavaScript dans la WebView de l'application :
// 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
}
}
}
Étape 2 : exposer un backend de résolution
Exécutez ce solveur Python sur votre machine de développement pendant l'exécution du test :
# 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)
Étape 3 : brancher Espresso sur la résolution CAPTCHA
// 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 injection
Thread.sleep(1000)
// Verify checkout completed
onView(withText("Order Confirmed")).check(
androidx.test.espresso.assertion.ViewAssertions.matches(isDisplayed())
)
}
}
Dépannage
| Probleme | Cause probable | Correctif |
|---|---|---|
10.0.2.2 est inaccessible |
Vous n'êtes pas sur l'émulateur Android | Utilisez l'IP réelle de la machine hôte sur appareil physique |
evaluateJavascript ne donne rien |
La WebView n'est pas encore chargée | Attendez WebViewClient.onPageFinished() avant d'exécuter le script |
addJavascriptInterface ne répond pas |
JavaScript est désactivé | Activez webView.settings.javaScriptEnabled = true |
| La requête réseau est bloquée | Android bloque le trafic HTTP clair | Autorisez android:usesCleartextTraffic="true" en debug seulement |
FAQ
Espresso peut-il piloter directement le contenu d'une WebView ?
Espresso fournit onWebView() pour certains cas simples, mais pas pour injecter du JavaScript arbitraire. Pour un CAPTCHA, il faut passer par evaluateJavascript() et l'API native de la WebView.
Est-ce que cela fonctionne sur de vrais appareils en CI ?
Oui. Il faut remplacer 10.0.2.2 par l'adresse IP de la machine qui exécute le backend et vérifier que l'appareil peut joindre ce service sur le réseau de test.
Comment éviter d'embarquer ce helper en production ?
Placez ce type de helper dans src/debug/java/. Les variantes Android excluent automatiquement ce source set des builds release.
Et pour reCAPTCHA Enterprise ?
L'approche reste la même, mais il faut fournir le sitekey Enterprise et, selon l'implémentation cible, ajouter les paramètres supplémentaires attendus par CaptchaAI.
Articles connexes
- Résoudre le callback reCAPTCHA v2 avec l'API
- Construire un pipeline de tests automatisé avec CaptchaAI
- Gérer reCAPTCHA v2 et Turnstile sur le même site
Prochaines étapes
Si vos tests mobiles butent sur une WebView protégée, récupérez votre clé API CaptchaAI et ajoutez un backend de résolution à votre environnement QA.
Guides associés :
- Gérer les CAPTCHA iOS avec XCUITest
- Gérer les CAPTCHA avec Appium sur mobile
- Extraire les paramètres reCAPTCHA depuis le code source