Use Cases

Kayıt Akışı Testinde CAPTCHA Kullanımı

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-TR yerel ayarlı sahte adlar ve +90 formatı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
  • sitekey veya pageurl, 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.

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