Integrations

XCUITest ve CaptchaAI ile iOS Otomasyonu CAPTCHA Kullanımı

XCUITest, WKWebView içinde JavaScript çalıştıramaz — bu yüzden giriş formu, ödeme ekranı veya kayıt akışı gibi gömülü bir web görünümünde reCAPTCHA v2 çıktığı an otomatik test orada kilitlenir. Çözüm üç parçadan oluşur: uygulamaya debug-only bir test kancası eklemek, sitekey'i JavaScript ile okumak ve CaptchaAI API'sinden aldığınız token'ı aynı yoldan geri enjekte etmek. CAPTCHA'yı CaptchaAI çözer, siz de kullanıcı arayüzü testinizi elle müdahale etmeden uçtan uca tamamlarsınız.

Bu rehberde WKWebView'deki CAPTCHA'nın nasıl tespit edileceğini, CaptchaAI üzerinden nasıl çözüleceğini ve token'ın web içeriğine nasıl geri yazılacağını adım adım görüyorsunuz.

Neden XCUITest'te WKWebView CAPTCHA'ları testi kilitliyor?

XCUITest yalnızca kullanıcı arayüzü öğeleriyle etkileşime girer, JavaScript değerlendiremez. WKWebView içinde çalışan reCAPTCHA v2 bu yüzden testi tıkıyor; aradaki boşluğu kapatmak için uygulamaya, yalnızca debug build'de derlenen bir test kancası eklemeniz gerekiyor.

Not: WebView'ı kontrol etmiyorsanız (ör. üçüncü taraf SDK içinde gömülü), bu rehberdeki uygulama-içi kanca yerine Appium'un execute_script yeteneğini kullanabilirsiniz — uygulama tarafında değişiklik gerektirmez.

Örnek senaryo: İstanbul merkezli bir ekibin CI testleri

Bir fintech uygulamasının kayıt formu WKWebView içinde açılıyor ve reCAPTCHA v2 içeriyor. Ekip bu formu Europe/Istanbul saat diliminde çalışan bir CI hattında (GitHub Actions ya da Xcode Cloud fark etmez) her gece test ediyor; test fixture'larındaki telefon alanları +90 formatında sentetik verilerle dolduruluyor. CAPTCHA, otomatik testi her seferinde durduruyor:

Aşama Ne yapar
1 Test çalışırken WebView'deki CAPTCHA'yı algılar
2 sitekey'i programatik olarak çıkarır
3 CaptchaAI üzerinden çözer
4 Formun gönderilebilmesi için token'ı enjekte eder

Ortam: Xcode 15+, Swift, XCUITest, macOS test çalıştırıcısı, CaptchaAI API.

Mimari: Test yardımcısı servisi nasıl çalışır

XCUITest, WKWebView içinde JavaScript'i doğrudan değerlendiremez; bu yüzden mimari, uygulamanın test sırasında çağırdığı bir yardımcı uç noktaya dayanır:

  • XCUITest — kullanıcı arayüzünü yönetir, test yardımcısı üzerinden CAPTCHA çözümünü tetikler.
  • Test Yardımcısı API'si — sitekey ve URL'yi alır, CaptchaAI'yi çağırır, token'ı döndürür.
  • Uygulama Test Kancası — WKWebView'de JavaScript değerlendirerek CAPTCHA'yı algılar ve enjekte eder.
  • CaptchaAI API'si — CAPTCHA sorununu çözer.

Adım 1: Uygulamaya debug-only bir test kancası ekleyin

Uygulamanızın WKWebView denetleyicisine, erişilebilirlik tanımlayıcıları veya URL şeması üzerinden tetiklenebilen bir test-modu CAPTCHA işleyicisi ekleyin. Kod yalnızca #if DEBUG bloğunda derlenir, dolayısıyla üretim sürümüne karışmaz:

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

Adım 2: CaptchaAI'ye bağlanan arka uç çözücüyü çalıştırın

Test sırasında CaptchaAI ile konuşan yerel bir çözücü servisini ayağa kaldırın. Servis sitekey'i alır, in.php'ye gönderir ve res.php üzerinden sonucu sorgular:

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

Not: reCAPTCHA v2 tipik olarak 60 saniyenin altında, yüksek başarı oranıyla çözülür. Kod da bu yüzden 30 kez, 5 saniye arayla sorguluyor — toplam 150 saniyelik bir tampon bırakarak yoğun CI koşularında da güvenli kalıyor.

Adım 3: XCUITest akışına entegre edin

XCUITest'inizde, CAPTCHA içeren WebView yüklendiğinde çözüm akışını tetikleyin ve token bekleme süresini gerçekçi tutun:

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

Test yardımcısı ile uygulama arasındaki sözleşme

Yardımcı servise giden istek yükünü (hedef URL ve sorgulama meta verileri dahil) net tanımlayın; yanıtı token, süre aşımı ve hata nedeniyle yapılandırılmış tutun ki test katmanı senaryolara göre temiz biçimde dallanabilsin. Simülatör günlükleriyle yardımcı servis günlüklerini aynı iz (trace) kimliğiyle eşleştirin — CI'da hata ayıklarken bu zaman kazandırır.

Sık karşılaşılan sorunlar ve çözümleri

evaluateJavaScript neden nil döndürüyor?

WebView, yüklemeyi henüz tamamlamadığında bu görülür. JS enjekte etmeden önce webView.isLoading == false olmasını bekleyin.

Simülatörden arka uca neden ulaşılamıyor?

localhost simülatör içinden her zaman erişilebilir değildir. 127.0.0.1'i veya Mac'in ağ IP'sini kullanın; App Transport Security ayarını kontrol edin.

Token enjeksiyonu callback'i neden tetiklemiyor?

reCAPTCHA callback'i genellikle karmaşık bir nesnede iç içe geçmiştir. ___grecaptcha_cfg.clients altındaki tüm özellikleri özyinelemeli gezin.

XCUITest çözüm beklerken neden zaman aşımına uğruyor?

CaptchaAI çözüm süresi standart bekleme aralığından uzun sürebilir. CAPTCHA içeren testler için zaman aşımını 120+ saniyeye ayarlayın.

SSS

XCUITest, WKWebView içindeki JavaScript'i doğrudan çalıştırabilir mi?

Hayır. XCUITest yalnızca kullanıcı arayüzü öğeleriyle etkileşime girer, JavaScript değerlendiremez. Bu boşluğu kapatmak için uygulama kodunda debug-only bir test kancasına ihtiyacınız var.

Debug-only test kancası üretime sızma riski taşır mı?

Hayır — kod #if DEBUG derleyici yönergesiyle sarılırsa release build'den tamamen çıkarılır. Yine de CI yapılandırmanızda release şemasını debug test kancasıyla asla derlememelisiniz.

Bu akış için hangi CaptchaAI planı yeterli?

Çoğu XCUITest çalıştırması simülatör başına sıralı ilerlediği için BASIC ($15/ay, 5 thread) genelde yeterlidir. Birden fazla simülatörü paralel çalıştıran büyük CI matrisleri için STANDARD ($30/ay, 15 thread) veya ADVANCE ($90/ay, 50 thread) daha rahat bir tampon sağlar.

reCAPTCHA v2 çözüm süresi test zaman aşımını nasıl etkiler?

reCAPTCHA v2 tipik olarak 60 saniyenin altında çözülür. Kod örneğindeki 120 saniyelik test zaman aşımı bu sürenin üstünde güvenli bir pay bırakır; daha kısa bir değer ara sıra gerçek bir çözümü zaman aşımına düşürebilir.

Test fixture'larında gerçek kullanıcı verisi kullanmalı mıyım?

Hayır. KVKK kapsamında gerçek kişisel veri yalnızca yetkili ve amaca uygun işlenmelidir; XCUITest fixture'larında sentetik +90 telefon numaraları ve örnek e-posta adresleri kullanmak hem daha güvenli hem daha tekrarlanabilir bir test seti sağlar.

İlgili Makaleler

reCAPTCHA v2 callback akışını API ile çözme rehberinde inceleyin, kod yazmadan kurulum için Zapier ile kodsuz CaptchaAI otomasyonuna bakın; tek sitede iki sağlayıcı çıkarsa reCAPTCHA v2 ile Turnstile'yi aynı akışta çözme rehberi işinize yarar.

Sonraki Adımlar

CaptchaAI'yi iOS test hattınıza entegre edin — API anahtarınızı alın ve CAPTCHA korumalı akışları elle müdahale etmeden otomatikleştirin.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.