Kayıt formunuzu koruyan reCAPTCHA v2 veya Cloudflare Turnstile, her sürümde regresyon testlerinizi ilk adımda durdurur — kayıt akışını otomatik doğrulamak isteyen QA ekipleri için asıl darboğaz budur.
Pratik çözüm doğrulamayı devre dışı bırakmak değil: test sırasında CAPTCHA token'ını bir API ile alıp forma enjekte etmek ve senaryoyu insan müdahalesi olmadan uçtan uca çalıştırmaktır. Bu rehber, Selenium tabanlı bir kayıt test paketini CaptchaAI ile nasıl kuracağınızı çalışan Python kodu üzerinden gösterir.
Bu akış yalnızca sahibi olduğunuz veya test etme izniniz bulunan uygulamalar içindir; üçüncü taraf sitelerde kayıt otomasyonu çalıştırmayın.
Kayıt testleri neden CAPTCHA çözümü gerektirir?
Kayıt formları, bot hesaplarını engellemek için neredeyse her zaman CAPTCHA ile korunur. Bu koruma kullanıcı için iyidir; ancak otomatik test tarafında formu her açtığınızda karşınıza çıkan doğrulama, test paketinizi daha ilk adımda bloke eder. Bir QA ekibinin kayıt akışında düzenli olarak doğrulaması gereken noktalar şunlardır:
- Kayıt akışının hem staging hem üretim ortamında sorunsuz çalıştığını doğrulamak
- Uç durumları test etmek: yinelenen e-posta, zayıf şifre, eksik veya hatalı alanlar
- Her dağıtımdan sonra regresyon testlerini otomatik çalıştırmak
- CAPTCHA widget'ının formun yerleşimini veya gönderimini bozmadığını doğrulamak
Türkiye'deki e-ticaret ve fintech ekipleri için bu özellikle kritiktir: her sürümde çalışan bir kayıt/onboarding akışı doğrudan dönüşüm oranını etkiler.
Manuel test bu hacimde sürdürülebilir değildir; bu yüzden CAPTCHA'yı test ortamında programatik olarak çözmek, akışın geri kalanını gerçek kullanıcı gibi ilerletmenin tek pratik yoludur.
Kayıt formlarında hangi CAPTCHA türleriyle karşılaşırsınız?
Kayıt sayfalarında en sık iki tür görürsünüz: onay kutusu tabanlı reCAPTCHA v2 (ve görünmez reCAPTCHA varyantı) ile Cloudflare Turnstile.
Her ikisi de çözüldüğünde tarayıcıya bir token döner; formun gizli alanına yazılan bu token, gönderimde sunucu tarafında doğrulanır. Test paketinizin yapması gereken tek şey, doğru token'ı doğru alana yerleştirmektir.
CaptchaAI'nin kayıt formlarında işinize yarayacak tür kapsamı şöyledir:
- Tam destekli: reCAPTCHA v2/v3, Cloudflare Turnstile ve Challenge, GeeTest v3, görüntü/OCR, grid ve BLS CAPTCHA
- Beta: CaptchaFox, Friendly Captcha ve Lemin
- Desteklenmiyor: hCaptcha ve FunCaptcha (Arkose Labs) — kayıt formunuz bunlardan birini kullanıyorsa test stratejinizi buna göre planlayın
Bu rehberdeki kod, reCAPTCHA v2 ve Turnstile'ı otomatik algılayıp uygun token alanını (g-recaptcha-response veya cf-turnstile-response) doldurur.
Test mimarisi: veri üretiminden hesap doğrulamaya
Paket dört aşamadan oluşur: test verisi üretimi, formun (uç durumlar dahil) doldurulması, CAPTCHA'nın çözülmesi ve hesabın oluşup oluşmadığının doğrulanması. Akışın tamamı şu sırayı izler:
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
Adım adım uygulama
1. Benzersiz test verisi üretin
Her test çalıştırmasında çakışmayan e-posta ve kullanıcı adları gerekir; aksi halde "yinelenen kayıt" hatası gerçek bir kusuru değil, yalnızca test verisi çakışmasını gösterir. Zaman damgası ve rastgele son ek kullanan basit bir üretici bunu sağlar. Örnek e-postalar @testmail.example.com gibi sahte bir alan adı kullanır; böylece gerçek kişisel veri işlenmez ve KVKK kapsamına giren bir durum oluşmaz.
import random
import string
import time
class TestUser:
def __init__(self, prefix="test"):
ts = int(time.time())
rand = ''.join(random.choices(string.ascii_lowercase, k=4))
self.first_name = f"{prefix}_{rand}"
self.last_name = "User"
self.email = f"{prefix}_{ts}_{rand}@testmail.example.com"
self.username = f"{prefix}_{ts}_{rand}"
self.password = f"Test!{ts}{rand.upper()}"
def as_dict(self):
return {
"first_name": self.first_name,
"last_name": self.last_name,
"email": self.email,
"username": self.username,
"password": self.password,
}
2. Kayıt test sınıfını kurun
Asıl iş bu sınıfta yapılır. CaptchaSolver, CaptchaAI'nin in.php ve res.php uç noktalarına istek göndererek token'ı alır; RegistrationTester ise Selenium ile formu doldurur, _solve_captcha() içinde sayfadaki widget'ı algılar ve dönen token'ı doğru gizli alana execute_script ile yazar.
Turnstile için cf-turnstile-response, reCAPTCHA v2 için g-recaptcha-response alanı hedeflenir. Görev gönderildikten sonra sonuç, hazır olana kadar birkaç saniyelik aralıklarla sorgulanır (periyodik sorgulama).
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class CaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve_recaptcha(self, sitekey, pageurl):
return self._solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
})
def solve_turnstile(self, sitekey, pageurl):
return self._solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
def _solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(result["request"])
raise TimeoutError("Timed out")
class RegistrationTester:
def __init__(self, api_key, base_url):
self.solver = CaptchaSolver(api_key)
self.base_url = base_url
self.driver = webdriver.Chrome()
self.wait = WebDriverWait(self.driver, 10)
self.results = []
def _fill(self, selector, value):
el = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
el.clear()
el.send_keys(value)
def _solve_captcha(self):
html = self.driver.page_source
page_url = self.driver.current_url
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
if turnstile:
sitekey = turnstile[0].get_attribute("data-sitekey")
token = self.solver.solve_turnstile(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha and "recaptcha" in html.lower():
sitekey = recaptcha[0].get_attribute("data-sitekey")
token = self.solver.solve_recaptcha(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
def _get_errors(self):
"""Collect any visible error messages on the page."""
error_selectors = [
".error", ".alert-danger", ".form-error",
"[role='alert']", ".validation-error",
]
errors = []
for sel in error_selectors:
for el in self.driver.find_elements(By.CSS_SELECTOR, sel):
text = el.text.strip()
if text:
errors.append(text)
return errors
def _check_success(self):
"""Check if registration succeeded."""
html = self.driver.page_source.lower()
url = self.driver.current_url.lower()
success_indicators = [
"welcome", "account created", "verify your email",
"registration successful", "thank you for registering",
]
return any(ind in html or ind in url for ind in success_indicators)
# --- Test Cases ---
def test_valid_registration(self):
"""Test: Valid registration should succeed."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='firstName'], #first-name", user.first_name)
self._fill("[name='lastName'], #last-name", user.last_name)
self._fill("[name='email'], #email", user.email)
self._fill("[name='username'], #username", user.username)
self._fill("[name='password'], #password", user.password)
confirm_fields = self.driver.find_elements(By.CSS_SELECTOR, "[name='confirmPassword'], #confirm-password")
if confirm_fields:
confirm_fields[0].clear()
confirm_fields[0].send_keys(user.password)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
success = self._check_success()
self.results.append({
"test": "valid_registration",
"passed": success,
"user": user.email,
"errors": self._get_errors() if not success else [],
})
return success
def test_duplicate_email(self):
"""Test: Duplicate email should show error."""
user = TestUser()
# First registration
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
# Second registration with same email
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", "Duplicate")
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
has_error = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "duplicate_email",
"passed": has_error,
"errors": errors,
})
return has_error
def test_weak_password(self):
"""Test: Weak password should be rejected."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", "123") # Weak password
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
rejected = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "weak_password",
"passed": rejected,
"errors": errors,
})
return rejected
def run_all(self):
"""Run all registration tests."""
tests = [
self.test_valid_registration,
self.test_duplicate_email,
self.test_weak_password,
]
for test_fn in tests:
try:
test_fn()
except Exception as e:
self.results.append({
"test": test_fn.__name__,
"passed": False,
"errors": [str(e)],
})
return self.results
def report(self):
passed = sum(1 for r in self.results if r["passed"])
total = len(self.results)
lines = [f"Registration Tests: {passed}/{total} passed", "-" * 40]
for r in self.results:
status = "PASS" if r["passed"] else "FAIL"
lines.append(f" [{status}] {r['test']}")
if r.get("errors"):
for err in r["errors"]:
lines.append(f" {err}")
return "\n".join(lines)
def close(self):
self.driver.quit()
Paketi çalıştırın
RegistrationTester'ı API anahtarınız ve staging URL'nizle başlatın, run_all() çağırın ve raporu yazdırın. try/finally bloğu, bir test başarısız olsa bile tarayıcının her koşulda kapatılmasını sağlar.
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
Çıktı:
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
Pytest ile CI/CD entegrasyonu
Test sınıfını doğrudan bir pytest fikstürü içine sararak mevcut test paketinize dahil edebilirsiniz. Fikstür scope="module" olarak tanımlandığında tek bir tarayıcı oturumu tüm testlerde yeniden kullanılır.
CI ortamında tarayıcıyı headless çalıştırın (options.add_argument("--headless")) ve API anahtarınızı koda gömmek yerine bir ortam değişkeni olarak sağlayın.
import pytest
@pytest.fixture(scope="module")
def tester():
t = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
yield t
t.close()
def test_valid_registration(tester):
assert tester.test_valid_registration(), "Valid registration should succeed"
def test_duplicate_email_rejected(tester):
assert tester.test_duplicate_email(), "Duplicate email should be rejected"
def test_weak_password_rejected(tester):
assert tester.test_weak_password(), "Weak password should be rejected"
Sorun giderme
Kayıt testleri kırılganlaşırsa sorun genellikle başarı göstergesinde, CAPTCHA zamanlamasında veya CSS seçicilerdedir. En sık görülen durumlar ve çözümleri:
| Sorun | Sebep | Düzeltme |
|---|---|---|
| Kayıt başarılı ama test başarısız raporluyor | Başarı göstergesi eşleşmiyor | Sitenizin başarı metnini _check_success() listesine ekleyin |
| CAPTCHA algılanmıyor | Widget gecikmeyle yükleniyor | _solve_captcha() çağrısından önce time.sleep(2) ekleyin |
| Form alanları bulunamıyor | Farklı HTML yapısı | CSS seçicileri kendi sitenize göre güncelleyin |
| Token reddediliyor | Token çok erken çözüldü, süresi doldu | _solve_captcha()'yi gönderim adımına yaklaştırın |
Türkiye'deki QA ekipleri için pratik notlar
Birkaç yerel değerlendirme bu akışı daha sağlam kılar:
- KVKK uyumu: Test verisi her zaman sentetik olmalı. Gerçek müşteri e-postaları veya kimlik bilgileri test ortamına girmemeli; yukarıdaki üretici gibi
tr-TRyerel ayarlı sahte adlar ve+90formatında sahte telefon numaralarıyla çalışın. - Öngörülebilir maliyet: CaptchaAI thread tabanlı fiyatlandırır ve ücreti USD üzerinden alır. TL'deki dalgalanmaya rağmen aylık maliyet sabit kaldığından, CI'da düzenli çalışan test paketleri için bütçeleme kolaydır. Çoğu QA hacmi için BASIC ($15/ay, 5 thread) yeterlidir.
- Staging önceliği: Testleri önce staging ortamında çalıştırın; üretimde yalnızca smoke testleri koşun ve oluşturulan test hesaplarını sonradan temizleyin.
Özetle: sentetik test verisi, staging önceliği ve düzenli hesap temizliği, KVKK açısından güvenli bir kayıt test akışının üç direğidir.
Sık sorulan sorular
CaptchaAI, reCAPTCHA v2 ve Turnstile dışında hangi CAPTCHA türlerini çözer?
reCAPTCHA v2/v3, Cloudflare Turnstile ve Challenge, GeeTest v3, görüntü/OCR, grid ve BLS CAPTCHA'larını çözer. CaptchaFox, Friendly Captcha ve Lemin beta olarak desteklenir. hCaptcha ve FunCaptcha (Arkose Labs) şu anda desteklenmez.
Kayıt testleri için hangi CaptchaAI planı yeterli olur?
Çoğu QA iş yükü için BASIC ($15/ay, 5 thread) yeterlidir; plan başına thread sayısı eşzamanlı çözüm kapasitenizi belirler. Testleri geniş bir CI matrisinde paralel çalıştırıyorsanız STANDARD ($30/ay, 15 thread) daha rahat nefes aldırır.
reCAPTCHA v2 token'ı neden geçersiz sayılıyor?
Genellikle şu üç nedenden biri söz konusudur:
- Token çözülüp forma yazıldıktan sonra gönderim gecikti ve süresi doldu
sitekeyveyapageurl, sayfadaki değerlerle birebir eşleşmiyor- Aynı token ikinci kez gönderildi
_solve_captcha() çağrısını gönderim tıklamasına mümkün olduğunca yaklaştırın ve değerlerin sayfayla eşleştiğinden emin olun.
Test verilerini KVKK'ya uygun biçimde nasıl yönetirim?
Yalnızca sentetik veri kullanın ve test hesaplarını çalıştırma sonunda silin. test_* gibi bir adlandırma kuralı, hesapları toplu tanımlayıp yönetici API'niz üzerinden temizlemeyi kolaylaştırır; gerçek kişisel veri hiçbir zaman test ortamına girmemelidir.
İlgili rehberler
Kayıt akışlarınızı CAPTCHA engeline takılmadan test edin — CaptchaAI ile başlayın.