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_scriptyeteneğ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: