Integrations

Pengujian CAPTCHA Android dengan Espresso dan CaptchaAI

CAPTCHA di dalam WebView adalah penghambat paling menjengkelkan dalam pengujian Espresso — matcher Espresso biasa tidak bisa mengetuk gambar, jadi alur checkout atau login langsung macet begitu reCAPTCHA v2 muncul. Solusinya ada tiga langkah:

  • Deteksi sitekey dan URL halaman lewat JavaScript yang dievaluasi di dalam WebView.
  • Selesaikan CAPTCHA lewat backend solver yang memanggil CaptchaAI.
  • Suntikkan token hasilnya kembali ke elemen #g-recaptcha-response sebelum test lanjut ke assertion berikutnya.

Panduan ini menjabarkan ketiga langkah itu — helper debug Kotlin, backend solver Python, dan skrip test Espresso yang menyatukannya — plus tabel troubleshooting untuk error yang paling sering muncul di device farm CI.

Kapan CAPTCHA WebView Menghambat Test Espresso Anda

Sebagian besar aplikasi e-commerce dan travel di Android tidak membangun ulang halaman checkout secara native. Mereka memuat halaman pihak ketiga di dalam WebView apa adanya, lengkap dengan proteksi CAPTCHA-nya. CAPTCHA WebView biasanya muncul di titik-titik berikut:

  • Checkout pembayaran pihak ketiga sebelum transaksi diproses.
  • Halaman login/SSO yang di-embed dari penyedia autentikasi eksternal.
  • Formulir pendaftaran yang dimuat dari backend web, bukan native form Android.

Saat test instrumented Espresso mencoba memverifikasi salah satu alur ini, CAPTCHA memblokirnya total karena Espresso tidak bisa "mengklik gambar" seperti manusia — dan tidak ada matcher bawaan untuk menyelesaikan reCAPTCHA v2.

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

Tip infrastruktur: tim QA yang menjalankan backend solver ini di CI sebaiknya deploy layanan Python ke region terdekat dengan runner-nya — AWS ap-southeast-3 (Jakarta) atau ap-southeast-1 (Singapura) — supaya latensi polling ke CaptchaAI tidak menambah waktu tunggu saat device farm menjalankan puluhan test paralel dengan batas waktu ketat.

Langkah 1: Tambahkan Helper Debug ke WebView Android

Buat helper khusus source set debug yang bisa mengevaluasi JavaScript di dalam WebView aplikasi Anda:

// 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: Bangun Backend Solver CAPTCHA dengan Python

Jalankan solver Python berikut di mesin development Anda selama eksekusi test — helper di Langkah 1 memanggilnya lewat 10.0.2.2, alamat loopback host khusus emulator Android:

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

Menyesuaikan Waktu Tunggu Polling

Loop for _ in range(30) di atas menunggu maksimal 150 detik (30 percobaan × 5 detik jeda) sebelum menyerah. reCAPTCHA v2 biasanya selesai dalam waktu di bawah 60 detik dengan tingkat keberhasilan tinggi pada tipe yang didukung, jadi ambang 150 detik memberi ruang aman untuk fluktuasi jaringan tanpa membuat test Espresso Anda menggantung terlalu lama. Kalau test suite CI Anda punya batas waktu ketat per test, dua opsi yang masuk akal:

  1. Perpendek total timeout (mis. 20 percobaan × 5 detik = 100 detik) dan biarkan test gagal cepat kalau solve benar-benar bermasalah.
  2. Perbesar jeda antar-polling menjadi 8–10 detik untuk mengurangi jumlah request ke res.php tanpa memperpendek total waktu tunggu.

Langkah 3: Satukan Semuanya di Test Espresso

Skrip test berikut memanggil helper WebView, menunggu deteksi CAPTCHA, memicu solve, lalu memverifikasi checkout selesai setelah token disuntikkan:

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

Pertanyaan Umum

Apa bedanya menangani CAPTCHA di WebView Espresso dengan otomasi native lewat Appium?

Pendekatan ini berlaku karena CAPTCHA muncul di WebView yang JavaScript-nya bisa dievaluasi langsung. Untuk automation native yang menyentuh komponen di luar WebView, pendekatan Appium biasanya lebih tepat — lihat panduan Appium di bagian bawah artikel ini.

Plan CaptchaAI apa yang cocok untuk pipeline CI dengan banyak device paralel?

CaptchaAI menagih per thread yang sedang aktif menyelesaikan CAPTCHA, bukan per solve — jadi ukuran plan yang tepat mengikuti jumlah device/emulator yang berjalan bersamaan, bukan jumlah test per hari:

Plan Harga Thread Cocok untuk
BASIC $15/bulan 5 Beberapa emulator lokal yang jalan bergantian
STANDARD $30/bulan 15 Device farm CI dengan test paralel penuh

Kalau thread lebih sedikit dari jumlah device yang solve CAPTCHA bersamaan, sebagian test akan mengantre menunggu thread kosong — bukan gagal, hanya lebih lambat.

Apakah pendekatan yang sama berlaku untuk reCAPTCHA v3 di WebView Android?

Ya, prinsipnya sama — deteksi sitekey, kirim ke CaptchaAI, suntikkan token. Bedanya v3 tidak menampilkan widget yang perlu diklik, jadi cukup sesuaikan captchaType di body request, karena v3 biasanya mengembalikan skor lewat callback JavaScript, bukan elemen #g-recaptcha-response.

Bagaimana cara mencegah helper pengujian ini ikut ke build rilis produksi?

Tempatkan CaptchaTestHelper di source set yang tepat, bukan di source set utama aplikasi:

  • src/debug/java/ — helper CAPTCHA masuk di sini, hanya ikut build debug.
  • src/main/java/ — kode produksi; jangan pernah taruh helper pengujian di sini.

Build variant Android otomatis mengecualikan source debug dari build rilis, jadi kode ini tidak pernah masuk ke APK yang dipublikasikan ke Play Store.

Bagaimana dengan reCAPTCHA Enterprise di WebView aplikasi Android?

Alurnya identik, tapi Anda perlu sitekey Enterprise dan mungkin parameter tambahan seperti enterprise: 1 ke CaptchaAI saat submit task.

Error WebView dan Espresso yang Sering Muncul

Masalah Penyebab Solusi
10.0.2.2 tidak terjangkau Tidak berjalan di Android Emulator Gunakan IP host asli untuk perangkat fisik; 10.0.2.2 khusus emulator
Callback evaluateJavascript bernilai null WebView belum selesai dimuat Tambahkan listener WebViewClient.onPageFinished() sebelum evaluasi
addJavascriptInterface tidak berfungsi JavaScript dinonaktifkan Set webView.settings.javaScriptEnabled = true
Request diblokir kebijakan Cleartext HTTP ke localhost di Android 9+ Tambahkan android:usesCleartextTraffic="true" di AndroidManifest.xml (khusus debug)
Solve selalu timeout di CI, lancar di lokal Runner CI di region jauh dari API CaptchaAI, latensi polling tinggi Deploy backend solver ke region terdekat (lihat tip infrastruktur di atas) atau perbesar total timeout

Kalau masalah tetap muncul setelah kelima perbaikan di atas, periksa log respons in.php — status selain 1 biasanya berarti sitekey atau pageurl yang terkirim ke CaptchaAI salah, bukan masalah di sisi WebView atau Espresso.

Artikel Terkait

Langkah Selanjutnya

Hentikan CAPTCHA WebView yang menghambat test suite Android Anda — ambil API key CaptchaAI dan pasang backend solver dalam hitungan menit.

Panduan terkait:

  1. Penanganan CAPTCHA Otomatisasi iOS dengan XCUITest
  2. Penanganan CAPTCHA dalam Otomatisasi Aplikasi Mobile dengan Appium
  3. Ekstraksi Parameter reCAPTCHA dari Sumber Halaman
Komentar dinonaktifkan untuk artikel ini.