Integrasi

Pengujian CAPTCHA Android dengan Espresso dan CaptchaAI

Pengujian UI Android yang dibuat dengan Espresso sering kali mengenai CAPTCHA di dalam WebViews – halaman login, formulir pendaftaran, atau alur pembayaran tersemat yang menampilkan reCAPTCHA v2.CaptchaAImemberikan penyelesaian terprogram sehingga rangkaian pengujian otomatis Anda dapat berjalan secara end-to-end tanpa interaksi CAPTCHA manual.

Panduan ini menunjukkan cara mendeteksi CAPTCHA di Android WebViews selama pengujian Espresso, menyelesaikannya melalui layanan backend, dan memasukkan token kembali ke halaman.

Skenario Dunia Nyata

Aplikasi Android Anda memuat halaman checkout pihak ketiga di WebView. Halaman ini menyajikan reCAPTCHA v2 sebelum mengizinkan pembayaran. Selama pengujian berinstrumen Espresso, CAPTCHA ini memblokir pengujian verifikasi pembayaran.

Lingkungan: Android Studio, Kotlin, Espresso, AndroidX Test, CaptchaAI API, backend Python.

Langkah 1: Buat Pembantu Tes di Aplikasi

Tambahkan pembantu khusus debug yang dapat mengevaluasi JavaScript di dalam WebView aplikasi:

// 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
        }
    }
}

Langkah 2: Layanan Pemecah Backend

Jalankan pemecah Python ini di mesin pengembangan Anda selama eksekusi pengujian:

# 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)

Langkah 3: Uji Espresso dengan Penanganan 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 pengaturan token
        Thread.sleep(1000)

        // Verify checkout completed
        onView(withText("Order Confirmed")).check(
            androidx.test.espresso.assertion.ViewAssertions.matches(isDisplayed())
        )
    }
}

Pemecahan Masalah

Masalah Penyebab Solusi
10.0.2.2 tidak dapat dijangkau Tidak menggunakan Emulator Android Gunakan IP host aktual untuk perangkat fisik; 10.0.2.2 khusus untuk emulator
Panggilan balik evaluateJavascript adalah nol WebView tidak dimuat sepenuhnya Tambahkan pendengar WebViewClient.onPageFinished() sebelum mengevaluasi
addJavascriptInterface tidak berfungsi JavaScript dinonaktifkan Hubungi webView.settings.javaScriptEnabled = true
Permintaan jaringan diblokir oleh kebijakan Cleartext HTTP ke localhost di Android 9+ Tambahkan android:usesCleartextTraffic="true" di AndroidManifest.xml (khusus debug)

Pertanyaan Umum

Bisakah Espresso berinteraksi dengan konten WebView secara langsung?

Espresso memiliki onWebView() untuk interaksi WebView dasar, tetapi Espresso tidak dapat mengevaluasi JavaScript sembarangan. Anda memerlukan evaluateJavascript() dari WebView API untuk penanganan CAPTCHA.

Apakah ini berfungsi pada perangkat nyata untuk CI?

Ya. Ganti 10.0.2.2 dengan IP sebenarnya dari mesin yang menjalankan backend solver. Pastikan perangkat dapat menjangkau backend melalui jaringan.

Bagaimana cara mencegah alat bantu pengujian dikirim ke produksi?

Tempatkan pembantu tes di set sumber src/debug/java/. Varian build Android secara otomatis mengecualikan sumber debug dari build rilis.

Bagaimana dengan reCAPTCHA Enterprise di aplikasi Android?

Pendekatannya serupa tetapi Anda memerlukan kunci situs Perusahaan dan mungkin perlu meneruskan parameter tambahan seperti enterprise: 1 ke CaptchaAI.

Artikel Terkait

  • Cara Mengatasi Callback Recaptcha V2 Menggunakan Api
  • Membangun Pipeline Pengujian Otomatis dengan CaptchaAI
  • Penanganan reCAPTCHA v2 dan Turnstile di Situs yang Sama

Langkah Selanjutnya

Otomatiskan pengujian CAPTCHA Android Anda — dapatkan kunci API CaptchaAI Anda dan siapkan backend pemecah.

Panduan terkait:

  • Penanganan CAPTCHA Otomatisasi iOS dengan XCUITest
  • Penanganan CAPTCHA dalam Otomatisasi Aplikasi Mobile dengan Appium
  • Mengekstrak Parameter reCAPTCHA dari Sumber Halaman
Komentar dinonaktifkan untuk artikel ini.