Çalıştırmayı Takılı Bırakmadan pytest’te CAPTCHA Nasıl Çözülür

pytest captcha - How to Solve CAPTCHAs in pytest Without Hanging the Run

pytest’te CAPTCHA çözmek için conftest.py dosyasında oturum başına bir CapSkip istemcisi oluşturun ve çözümü her testin içinde yapın, çünkü bir token yalnızca bir kez işe yarar. CapSkip çalışmıyorsa CAPTCHA testlerini atlayın. Ardından SDK’ya kendi zaman aşımlarını verin. Windows’ta pytest-timeout yalnızca thread yöntemini kullanabilir ve bu yöntem tetiklendiğinde tüm çalıştırmayı sonlandırır. Python SDK’sı hiçbir HTTP zaman aşımı ayarlamaz, bu yüzden sessiz kalan bir CapSkip bir testi hata vermeden takılı bırakır. CapSkip kendi Windows bilgisayarınızda çalışır, dolayısıyla bir gecede yüzlerce kez çözüm yapan bir test paketinin çözüm başına maliyeti yoktur. Bu pytest CAPTCHA kurulumu bu tuzakların her birini, ayrıca xdist ile paralel çalıştırmaları ve async testleri kapsar.

Neye ihtiyacınız var

  • reCAPTCHA çözücüsü açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatıyor.
  • Python 3.10 veya üzeri. capskip 1.3.0 paketi de pytest 9 de bunu gerektirir. Aşağıdaki her şey Windows 11 üzerinde Python 3.14.0 ile; pytest 9.1.1, pytest-timeout 2.4.0, pytest-xdist 3.8.0, pytest-asyncio 1.4.0 ve requests 2.34.2 ile çalıştırıldı. Reddedilen bağlantı süreleri 3.13.7 üzerinde de aynı çıktı.
  • Bir PowerShell istemi. Komutlar PowerShell sözdizimini kullanır.
  • Sahibi olduğunuz, giriş formunda reCAPTCHA v2 onay kutusu bulunan bir staging sitesi. Örnek, bir CSRF token’ı taşıyan Django tarzı bir forma giriş yapar. Alan adlarını kendi sitenize uyacak şekilde değiştirin.
  • Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki testler onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.

Aşağıdaki ölçümler, aynı bilgisayardaki sahte (mock) bir CapSkip ve sahte bir giriş sayfasından, gerçek pytest ve gerçek SDK ile alındı. pytest davranışı ve SDK davranışı kendi siteniz için de geçerlidir. Çözüm süreleri, thread sınırları ve giriş sayfasının kabul ve ret kuralları mock’un kendisine aittir.

1. Adım: projeyi oluşturun

Her şeyi tek bir proje klasörüne koyun: conftest.py, tokens.py ve test dosyaları. Bir sanal ortam kullanın. Aşağıdaki her komut, sanal ortamın kendi Python’unu yoluyla çalıştırır; bu yüzden ortamı etkinleştirmeniz gerekmez. pytest’i python -m pytest olarak başlatın. Bu biçim geçerli klasörü import yoluna ekler. conftest.py dosyasını bir tests klasörüne taşırsanız tokens.py dosyasını proje klasöründe tutun. Aksi halde pytest.exe, ModuleNotFoundError: No module named ‘tokens’ hatasıyla başarısız olur, python -m pytest ise çalışır.

# Windows PowerShell, in your project folder
python -m venv .venv
.venv\Scripts\python -m pip install pytest "capskip==1.3.0" requests

2. Adım: bütün çalıştırma için tek bir CapSkip istemcisi

Bunu conftest.py olarak kaydedin. CapSkip ayarlarını ortamdan okur, çünkü SDK bunları kendi başına okumaz; varsayılan değerler sayesinde hiçbir kurulum yapmadan çalışır. CAPSKIP_API_KEY yalnızca CapSkip’te API key validation’ı açtıysanız önemlidir. Fixture, tüm oturum için tek bir istemci oluşturur. Ayrıca CapSkip’in çalışıp çalışmadığını da öğrenir.

# conftest.py (next to your tests): pip install pytest capskip requests
import os
import socket

import pytest
from capskip import CapSkip

# The SDK reads no environment variables, so the fixture reads them.
HOST = os.environ.get("CAPSKIP_HOST", "127.0.0.1")
PORT = int(os.environ.get("CAPSKIP_PORT", "8080"))
API_KEY = os.environ.get("CAPSKIP_API_KEY", "capskip")


@pytest.fixture(scope="session")
def capskip_client():
    # CapSkip has no ping endpoint, so try a plain TCP connect. Use the IP,
    # not "localhost": a refused connect takes 2 s, or 4 s for localhost.
    try:
        socket.create_connection((HOST, PORT), timeout=3).close()
    except OSError as exc:
        pytest.skip(f"CapSkip is not running on {HOST}:{PORT}: {exc}")
    # One client for the whole run. A solve gives up after 90 s of polling.
    return CapSkip(apiKey=API_KEY, host=HOST, port=PORT, recaptchaTimeout=90)

CapSkip’in ping endpoint’i yoktur; bu yüzden düz bir TCP bağlantısı dinleyen bir şey olup olmadığını sorar ve hiçbir çözüm işi göndermez. Hiçbir şey dinlemiyorsa fixture başarısız olmak yerine testleri atlar. CapSkip kapalıyken 127.0.0.1’e yapılan reddedilmiş bir bağlantı Windows’ta 2.05 saniye, localhost adına ise yaklaşık 4.1 saniye sürdü. Atlama sonucu önbelleğe alınır, dolayısıyla tüm oturum bu maliyeti yalnızca bir kez öder: iki dosyadaki dört CAPTCHA testi toplam 2.05 saniye sürdü.

IP adresini kullanın. localhost adının, CapSkip çalışırken bile ikinci bir maliyeti vardır. Yalnızca IPv4 üzerinde dinleyen bir sunucuya karşı iki giriş testi localhost ile 10.3 saniye, 127.0.0.1 ile 3.75 saniye sürdü. Bu, bağlantı denetimi için bir kez yaklaşık 2 saniye ve her çözüm için 1 ila 2.5 saniye daha demektir.

3. Adım: bir kez çözen ve bir kez yeniden deneyen bir yardımcı

Bunu tokens.py olarak kaydedin. Bazı çözümler başarısız olur ve yeni bir deneme işe yarayabilir. SDK, mesajı ERROR_CAPTCHA_UNSOLVABLE olan bir ApiException fırlatır; ancak CapSkip’te bir Error Replacement metni ayarladıysanız mesaj o metni taşır ve aşağıdaki denetim hiçbir zaman eşleşmez. Yardımcı yalnızca bu hatayı yeniden dener. CapSkip’in reddettiği bir gönderim, örneğin ERROR_GOOGLEKEY ya da ERROR_PAGEURL, hemen hata fırlatır. Google’ın reddettiği bir sitekey de UNSOLVABLE olarak okunur, bu yüzden başarısız olmadan önce iki kez denenir. Hatanın ne anlama geldiği için ERROR_CAPTCHA_UNSOLVABLE sayfasına bakın.

# tokens.py: the helper your tests (and your app) call
from capskip import ApiException


def fresh_token(solver, sitekey, url, attempts=2):
    """Solve a reCAPTCHA v2 and return the token string."""
    for attempt in range(attempts):
        try:
            return solver.recaptcha(sitekey=sitekey, url=url)["code"]
        except ApiException as exc:
            # UNSOLVABLE is how CapSkip reports a solve that failed this
            # time. A refused submit (ERROR_GOOGLEKEY, ERROR_PAGEURL) will
            # fail again, so it raises at once.
            last = attempt == attempts - 1
            if last or "ERROR_CAPTCHA_UNSOLVABLE" not in str(exc):
                raise

4. Adım: test başına bir fixture ve bir marker

Sonraki bloğu conftest.py dosyasının sonuna ekleyin. Bu bir fabrikadır, token değil. Ona yapılan her çağrı yeni bir çözümdür ve her test kendi çözümünü ister, çünkü bir reCAPTCHA token’ı yalnızca bir kez kabul edilir. Python dosyanın ortasında import yapılmasına izin verir, bu yüzden import satırı bloğun koyduğu yerde kalabilir.

from tokens import fresh_token


@pytest.fixture
def solve_recaptcha(capskip_client):
    # A factory, not a token: every call is a new solve, because a
    # reCAPTCHA token is accepted once. Function scope, so each test
    # asks for its own.
    def solve(sitekey, url):
        return fresh_token(capskip_client, sitekey, url)

    return solve

Ardından aşağıdaki marker kancasını conftest.py dosyasına ekleyin. captcha adlı bir marker kaydeder; böylece hızlı testleri CapSkip olmadan, canlı olanları ise onunla çalıştırabilirsiniz. 6. adımın açtığı strict markers seçeneği açıkken, kaydetmediğiniz her marker bir hatadır. captcha’yı kaydetmek gerçek olanın çalışmasını sağlar ve yanlış yazımı hataya çevirir.

def pytest_configure(config):
    # Tell pytest the marker exists, so --strict-markers accepts it and a
    # misspelling such as "captcah" is an error instead of a silent mark.
    config.addinivalue_line(
        "markers", "captcha: needs a running CapSkip and solves a real CAPTCHA"
    )

5. Adım: giriş testlerini yazın

Sonraki iki bloğu test_login.py adlı tek bir dosya olarak kaydedin. Bu ilk blok sizindir, CapSkip’in değil. Giriş sayfasını açar, CSRF token’ını ve sitekey’i okur ve formu gönderir. requests bir form gönderiminde Referer göndermez. HTTPS üzerinden çalışan Django gibi katı bir site Referer olmayan bir gönderimi reddeder ve mock da aynısını 403 ile yaptı. Bu yüzden yardımcı onu ayarlar. İki deseni ve alan adlarını kendi HTML yapınıza göre ayarlayın ve post_form içine bir test hesabının e-postasını ve parolasını koyun.

# test_login.py: set LOGIN_URL to your own login page
import os
import re

import pytest
import requests

LOGIN_URL = os.environ.get("LOGIN_URL", "https://example.com/login")


def open_form(session):
    html = session.get(LOGIN_URL, timeout=15).text
    csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', html)
    sitekey = re.search(r'data-sitekey="([^"]+)"', html)
    return csrf.group(1), sitekey.group(1)


def post_form(session, csrf, token, password="secret"):
    # requests sends no Referer, and a Django site refuses an HTTPS post
    # that has none, so set it by hand.
    return session.post(LOGIN_URL, timeout=15, headers={"Referer": LOGIN_URL}, data={
        "csrfmiddlewaretoken": csrf,
        "email": "[email protected]",
        "password": password,
        "g-recaptcha-response": token,
    })

Testler çözümü en son yapar ve hemen gönderir, çünkü token yaklaşık iki dakika sonra sona erer. reCAPTCHA token süresi rehberi bu konunun ayrıntısını anlatıyor. Welcome, 403 ve hata metnini kendi sitenizin gösterdiği değerlerle değiştirin. Formunuz hesap oluşturuyorsa sabit bir kullanıcı adı ikinci gece çalıştırmasında çakışır, bu yüzden her seferinde yeni bir tane kullanın.

@pytest.mark.captcha
def test_login(solve_recaptcha):
    session = requests.Session()
    csrf, sitekey = open_form(session)
    token = solve_recaptcha(sitekey, LOGIN_URL)  # solve last, post at once
    resp = post_form(session, csrf, token)
    assert resp.status_code == 200 and "Welcome" in resp.text


@pytest.mark.captcha
def test_wrong_password_is_refused(solve_recaptcha):
    session = requests.Session()
    csrf, sitekey = open_form(session)
    token = solve_recaptcha(sitekey, LOGIN_URL)  # a new solve, not test 1's
    resp = post_form(session, csrf, token, password="wrong")
    assert resp.status_code == 403
    assert "Invalid email or password" in resp.text

6. Adım: önce hızlı testleri, sonra canlı olanları çalıştırın

İlk satır testleri giriş sayfanıza yöneltir. İkinci satır yanlış yazılmış bir marker’ı hataya çevirir. pytest’in ek seçenekler için okuduğu bir ortam değişkenini ayarlar ve bu değişken o PowerShell oturumunun geri kalanı boyunca geçerli kalır. Üçüncü satır CAPTCHA testleri dışındaki her şeyi çalıştırır. Dördüncü satır yalnızca canlı olanları çalıştırır. Atlama bayrağı bir testin neden atlandığını yazdırır, süre bayrağı ise en yavaş testleri listeler.

# Windows PowerShell: your login page, then make a misspelled marker an error
$env:LOGIN_URL = "https://staging.example.com/login"
$env:PYTEST_ADDOPTS = "--strict-markers"
.venv\Scripts\python -m pytest -m "not captcha"      # fast suite, no CapSkip
.venv\Scripts\python -m pytest -m captcha -rs -v --durations=5   # live tests

Bu noktada hızlı çalıştırma çalıştıracak bir şey bulamaz ve pytest 5 koduyla çıkar. Aşağıdaki birim testleri bölümünden test_unit.py dosyasını kaydettiğinizde testleri olur. O zaman çözücüden hiçbir şeye ihtiyaç duymaz: dört testi CapSkip kapalıyken 0.02 saniyede geçti. Canlı çalıştırma, conftest.py’yi değil testi gösteren şuna benzer bir satırla atlandı:

  • SKIPPED [1] test_login.py:29: CapSkip is not running on 127.0.0.1:8080: [WinError 10061] No connection could be made because the target machine actively refused it

Atlanan bir test paketi 0 koduyla çıkar ve pytest 9.1.1’de bir atlamayı başarısızlığa çeviren bir bayrak yoktur. Build’iniz CapSkip gerektiriyorsa junitxml seçeneğiyle bir junit dosyası yazın ve atlanan sayısı sıfırın üzerindeyse build’i başarısız sayın. Strict seçeneği açıkken bir testte captcha yerine captcah yazmak, toplama aşamasını 2 çıkış koduyla ve ‘captcah’ not found in `markers` configuration option mesajıyla durdurur. Strict seçeneği olmasa yazım hatası yalnızca bir uyarı olurdu ve o test hızlı test paketinde çalışırdı.

Çözümü her testin içinde yapın

Bir fixture’da bir kez çözüp token’ı paylaşmayın. Testte, module kapsamlı bir fixture bir kez çözdü ve iki test aynı token’ı gönderdi. İlki kabul edildi. İkincisi mock’un reCAPTCHA failed: token already used mesajıyla başarısız oldu. Gerçek bir site bunu farklı ifade eder. Hangi testin kaybettiği sıraya bağlıdır, bu yüzden hata yer değiştirir ve kararsız (flaky) görünür. xdist altında daha da kötüdür, paralel bölüm bunu gösteriyor. İşte hatanın tamamı, tanıyabilmeniz için. Başarısız olduğunu gördükten sonra dosyayı silin, yoksa sonraki çalıştırmaların rastgele başarısız olmasına ya da geçmesine yol açar.

# test_shared_token.py: the mistake, so you can see it fail
import pytest
import requests

from test_login import LOGIN_URL, open_form, post_form


@pytest.fixture(scope="module")
def shared_token(capskip_client):
    csrf, sitekey = open_form(requests.Session())
    return capskip_client.recaptcha(sitekey=sitekey, url=LOGIN_URL)["code"]


@pytest.mark.captcha
@pytest.mark.parametrize("n", [1, 2])
def test_each_login_reuses_one_token(shared_token, n):
    session = requests.Session()
    csrf, sitekey = open_form(session)
    resp = post_form(session, csrf, shared_token)
    assert resp.status_code == 200, resp.text  # n=2 fails: used already

Test başına bir çözüm, her biri birkaç saniyeye mal olur. CAPTCHA’nın kendisine ihtiyaç duymayan bir test fixture’ı istememelidir. Aşağıdaki sahte çözücü (fake) onun etrafındaki mantığı kapsar.

Takılan bir çözümü çalıştırmayı sonlandırmak yerine başarısız yapın

Windows’ta farklı olan kısım burasıdır. pytest-timeout eklentisinin iki yöntemi vardır. signal yöntemi SIGALRM gerektirir ve Windows’ta SIGALRM yoktur, bu yüzden orada kullanılamaz. Onu istemek INTERNALERROR ve AttributeError: module ‘signal’ has no attribute ‘SIGALRM’ ile sonuçlandı. thread yöntemi varsayılandır ve çalışan tek yöntemdir. Bir test takıldığında, 10 saniyelik sınırla ve çalıştırmada dört testle yaptığı şuydu:

  • Ana thread’in yığınını (stack) yaklaşık 100 satır olarak yazdırdı ve süreci 12.5 saniyede, 1 çıkış koduyla sonlandırdı.
  • Takılan teste PASSED ya da FAILED satırı verilmedi.
  • Hiçbir özet yazdırılmadı ve hiçbir junit dosyası yazılmadı.
  • Ondan sonraki iki test hiç çalışmadı.

xdist ile hasar daha küçüktür. Yalnızca o testi çalıştıran worker ölür. Test, worker gw1 crashed while running mesajıyla FAILED olarak raporlanır, yerine yeni bir worker başlar ve çalıştırma bir özetle ve 1 çıkış koduyla biter.

Yani thread zaman aşımı bir son çaredir, bir test zaman aşımı değil. SDK’nın önce, sıradan bir istisnayla başarısız olmasını istersiniz; böylece test başarısız olur ve çalıştırma devam eder. Python SDK 1.3.0 bunu zorlaştırır. requests’i zaman aşımı olmadan çağırır, bu yüzden bir çağrıyı kabul edip hiç yanıt vermeyen bir CapSkip onu takılı bırakır. Hiçbir değişiklik yapılmadığında, sessiz bir gönderim biz onu 45. saniyede durdurduğumuzda hâlâ engelliydi. recaptchaTimeout yalnızca yoklamalar arasında kontrol edilir ve takılı kalan bir yoklama henüz hazır değil olarak yorumlanır.

Düzeltme, SDK’nın kullandığı iki istek fonksiyonunu zaman aşımı olanlarla değiştirmektir. Sonraki bloğu conftest.py dosyasına ekleyin. SDK’nın içine uzanır, bu yüzden capskip’i 1.3.0 sürümüne sabitleyin. hasattr satırı, sonraki bir sürüm requests kullanmayı bırakırsa bloğun hiçbir şey yapmamasını sağlar. URL olarak verilen bir görüntü CAPTCHA’sının indirilmesini kapsamaz, çünkü SDK bunu kendi çağrısıyla yapar.

import functools
import types

import capskip.api
import requests

# capskip 1.3.0 sets no HTTP timeout, so a CapSkip that accepts a call
# and never answers would hang the test. Give every SDK call 5 s to
# connect and 30 s to answer. This swaps a name inside the SDK, so pin
# capskip==1.3.0 and do nothing if the SDK stops using requests.
if hasattr(capskip.api, "requests"):
    capskip.api.requests = types.SimpleNamespace(
        RequestException=requests.RequestException,
        post=functools.partial(requests.post, timeout=(5, 30)),
        get=functools.partial(requests.get, timeout=(5, 30)),
    )

Yamayla birlikte, gönderime hiç yanıt vermeyen bir CapSkip, 30.0 saniyede Read timed out. (read timeout=30) mesajlı bir NetworkException fırlattı. pytest testi 30.8 saniyede başarısız oldu ve çalıştırma bozulmadan kaldı. Sessiz bir yoklama farklıdır. SDK onu henüz hazır değil olarak yutar ve 300 saniyelik varsayılan recaptchaTimeout, 333.0 saniyede TimeoutException ile sona erdi. 2. adımın recaptchaTimeout=90 vermesinin nedeni budur. 90 ile test 92.6 saniyede başarısız oldu.

Süre sınırı yaklaşık 35 saniyeye kadar aşılabilir. Yoklamalar arasında kontrol edilir, tek bir yoklama tam 30 saniye sürebilir ve SDK kontrol etmeden önce 5 saniyeye kadar bekler. 45 sınırıyla başarısızlık 60.8 saniyede, 20 ile 30.3 saniyede geldi. Dolayısıyla bu sayılarla tek bir deneme için en kötü durum yaklaşık 125 saniyedir: yoklama için 90, artı takılı kalan bir yoklama. 3. adımdaki yardımcı en fazla iki deneme yapar, bu yüzden en kötü durum yaklaşık 250 saniyedir. pytest-timeout sınırını bunun üzerine koyun. Sonraki blok eklentiyi kurar ve 300 saniye ayarlar. Ortam değişkeni 6. adımdakinin yerini alır, bu yüzden strict seçeneğini yeniden yazar. Aynı iki seçeneği, timeout ve timeout_method olarak bir pytest ini dosyasına da koyabilirsiniz. Değişkeni yapıştırmak yalnızca daha kolaydır.

# Windows PowerShell: pytest-timeout has only the thread method here.
# 300 s is above two attempts at the slowest SDK failure (about 125 s each),
# so the SDK fails first.
.venv\Scripts\python -m pip install pytest-timeout
$env:PYTEST_ADDOPTS = "--strict-markers --timeout=300 --timeout-method=thread"

Testleri xdist ile paralel çalıştırın

Çözmek yavaştır ve çoğunlukla beklemeden ibarettir, bu yüzden xdist yardımcı olur. Her xdist worker’ı ayrı bir süreçtir. Bu yüzden bir session fixture’ı worker başına bir kez çalışır ve her worker kendi istemcisini oluşturur. Dört worker ve sekiz testle, çalıştırma dört süreç kimliği ve dört istemci nesnesi gösterdi. xdist olmadan her birinden bir tane vardır. Test, workers-0.log ile workers-7.log arasındaki dosyaları yazar ve Get-Content workers-*.log bunları gösterir. Worker başına bir istemci sorun değildir, çünkü istemci worker’ların paylaşması gereken hiçbir şeyi tutmaz.

# test_workers.py: shows one client per worker process
import os
from pathlib import Path

import pytest


@pytest.mark.captcha
@pytest.mark.parametrize("n", range(8))
def test_which_client(capskip_client, worker_id, n):
    line = f"{worker_id} pid={os.getpid()} client={id(capskip_client)}"
    Path(f"workers-{n}.log").write_text(line)

Worker sayısını kendiniz seçin. auto ayarı, CapSkip’in aynı anda çözebileceği sayıyı değil, CPU sayısını ifade eder. Test bilgisayarında 24 worker verdi. Dört birim testi onunla yaklaşık 4 saniye, dört worker ile yaklaşık 1.1 saniye sürdü; bunun nedeni worker başlatma maliyetidir. CapSkip’in kendi sınırı, varsayılanı 10 olan reCAPTCHA Max. Threads ayarıdır. Bu tablo, bu sınıra sahip bir mock’a karşı yapılan 24 girişten ve 5 saniyelik bir çözümden alındı.

Worker sayısıÇalışma süresiAynı anda çözüm
1189.6 s1
449.1 s4
1026.1 s10
1224.1 s10
2423.1 s10

10 worker’ın ötesinde kazanç küçüktür: 10 worker ile 26.1 saniye, 24 ile 23.1. Fazla işler CapSkip içinde boş bir thread bekler ve Wait Timeout süresini aşan bir iş başarısız olur. Küçültülmüş bir testte, sekiz test 2 sınırına, 6 saniyelik bir bekleme sınırına ve 4 saniyelik bir çözüme karşı aynı anda çalıştı. Dört iş ERROR_CAPTCHA_UNSOLVABLE ile başarısız oldu. tokens.py içindeki yeniden deneme onları yeniden sıraya aldı ve sekiz testin hepsi 22.3 saniyede geçti. Yeniden denemenin ikinci nedeni budur.

Her test, 5 saniyelik bir çözüm için yaklaşık 7.9 saniye sürdü. SDK 0, 0.25, 0.75, 1.75, 3.75 ve 7.75. saniyelerde yoklama yapar, bu yüzden biten bir çözümü bir sonraki yoklamada fark eder. xdist kurulumu ve komut aşağıdadır.

# Each xdist worker is its own process, so each builds its own client.
# Pick the number yourself: -n auto means CPU count, not solver capacity.
.venv\Scripts\python -m pip install pytest-xdist
.venv\Scripts\python -m pytest -m captcha -n 4 -v

Bir tuzak daha. xdist altında, daha önceki paylaşılan token hatası geçebilir. Module kapsamlı token worker başına bir kez çözülür, bu yüzden iki test yalnızca aynı worker’a düştüğünde çakışır. Bazı çalıştırmalarda geçen, bazılarında başarısız olan bir test, paylaşılan bir token aramanız için bir ipucudur.

AsyncCapSkip ile async testler

Python SDK’sının gerçek bir async istemcisi vardır: AsyncCapSkip. httpx üzerine kuruludur. Bu fixture’ı conftest.py dosyasına ekleyin. capskip_client ister, bu yüzden atlama denetimi yine geçerli olur. Async istemci bir event loop tutmaz, bu yüzden düz bir fixture yeterlidir ve testler varsayılan, fonksiyon kapsamlı loop’larda çalışır.

from capskip import AsyncCapSkip


@pytest.fixture(scope="session")
def async_capskip_client(capskip_client):
    # Asking for capskip_client keeps the skip-when-closed check. The
    # async client holds no event loop, so a plain fixture is enough.
    return AsyncCapSkip(apiKey=API_KEY, host=HOST, port=PORT, recaptchaTimeout=90)

pytest-asyncio 1.4.0 strict modda çalışır ve pytest 9, marker’ı olmayan bir async testi Failed: async def functions are not natively supported ile başarısız kılar. Sonraki bloğu test_async.py olarak kaydedin. YOUR_SITEKEY değerini sayfanın sitekey’iyle değiştirin.

# test_async.py: .venv\Scripts\python -m pip install pytest-asyncio
import asyncio

import pytest

from test_login import LOGIN_URL


@pytest.mark.captcha
@pytest.mark.asyncio  # needed: pytest-asyncio runs in strict mode
async def test_five_solves_at_once(async_capskip_client):
    solves = [
        async_capskip_client.recaptcha(sitekey="YOUR_SITEKEY", url=LOGIN_URL)
        for _ in range(5)
    ]
    results = await asyncio.gather(*solves)
    assert len({r["code"] for r in results}) == 5  # five different tokens

Doğrusal bir ölçeklenme beklemeyin. 1.5 saniyede çözen bir mock’a karşı 1, 5, 10 ve 25 çözümü gather ile toplamak 2.6, 2.5, 4.6 ve 9.8 saniye sürdü. İstemci her çağrı için yeni bir httpx bağlantısı açar; bu da event loop’u her seferinde kısa bir an meşgul eder. Zaman aşımı bölümündeki yama bu istemciye uygulanmaz. Sınırı, çözüm başına değil istek başına olan httpx’in 5 saniyelik varsayılanıdır, bu yüzden 12 saniyelik bir çözüm yine de çalıştı ve 14.1 saniyede bitti. Sessiz bir gönderim, 5.2 saniyede boş mesajlı bir NetworkException fırlattı. Sessiz bir yoklama, 20 sınırıyla 24.7 saniyede TimeoutException ile sona erdi. Python’da eşzamanlılık hakkında daha fazlası için şu rehbere bakın: asyncio ile paralel CAPTCHA çözme.

Çözücüye ihtiyaç duymayan birim testleri

Testlerinizin çoğunun CapSkip’e ihtiyacı olmamalı. Çözücüyü çağıran kendi kodunuza ait testler bir fake kullanabilir. Fake’in sonucu gerçeğiyle tam olarak eşleşmelidir. SDK, captchaId ve code içeren düz bir dict döndürür, bu yüzden fake de öyle yapar. Son test diğer yolu gösterir: çözene kadar hiçbir bağlantı açmayan gerçek istemciyi oluşturun ve bir metodu monkeypatch ile değiştirin. Dördü de CapSkip kapalıyken geçer. Bloğu test_unit.py olarak kaydedin.

# test_unit.py: runs with CapSkip closed
import pytest
from capskip import ApiException, CapSkip

from tokens import fresh_token

URL = "https://example.com/login"


class FakeSolver:
    """Same call and same result shape as CapSkip.recaptcha()."""

    def __init__(self, *outcomes):
        self.outcomes = list(outcomes)
        self.calls = 0

    def recaptcha(self, sitekey, url, **kwargs):
        self.calls += 1
        outcome = self.outcomes.pop(0)
        if isinstance(outcome, Exception):
            raise outcome
        return {"captchaId": "1001", "code": outcome}


def test_returns_the_code():
    assert fresh_token(FakeSolver("tok-1"), "KEY", URL) == "tok-1"


def test_retries_once_when_unsolvable():
    fake = FakeSolver(ApiException("ERROR_CAPTCHA_UNSOLVABLE"), "tok-2")
    assert fresh_token(fake, "KEY", URL) == "tok-2"
    assert fake.calls == 2


def test_does_not_retry_a_bad_sitekey():
    fake = FakeSolver(ApiException("ERROR_GOOGLEKEY"), "never-used")
    with pytest.raises(ApiException):
        fresh_token(fake, "KEY", URL)
    assert fake.calls == 1


def test_real_client_with_monkeypatch(monkeypatch):
    solver = CapSkip()  # opens no connection until a solve is called
    fake = {"captchaId": "1", "code": "tok-3"}
    monkeypatch.setattr(solver, "recaptcha", lambda **kw: fake)
    assert fresh_token(solver, "KEY", URL) == "tok-3"

Testleri başka bir makinedeki CapSkip’e karşı çalıştırın

Testlerin ve çözücünün aynı bilgisayarı paylaşması gerekmez. Bağlantı ayarlarından CapSkip’i Server moduna alın; ağ adresinizi veya genel IP’nizi dinler. Kararlı uzaktan erişim için statik bir genel IP önerilir. Ardından üç değişkeni ayarlayın ve aynı test paketi değişmeden çalışır. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.

# CapSkip on another Windows machine in Server mode: set these first
$env:CAPSKIP_HOST = "192.168.1.50"
$env:CAPSKIP_PORT = "8080"
$env:CAPSKIP_API_KEY = "your-key"   # only if API key validation is on
.venv\Scripts\python -m pytest -m captcha -rs

İki şeye dikkat edin. SDK, CapSkip ile düz HTTP üzerinden konuşur, bu yüzden API anahtarı açık metin olarak taşınır. Bu bağlantıyı özel bir ağda ya da bir VPN içinde tutun ve API key validation’ı açın. Ayrıca CapSkip, Server modu başladığında portunu her adrese açar. Erişimi beklediğiniz makinelerle sınırlamak için diğerleri için bir Windows Firewall engelleme kuralı ekleyin. Uzaktan kurulum bu rehber için çalıştırılmadı. Yalnızca değişkenler okundu.

Tam conftest.py

İşte 2. ve 4. adımlardaki conftest.py, zaman aşımı yaması ve async fixture ile birlikte tek bir dosyada. Tek tek eklediğiniz blokların yerini alır, bu yüzden önce onları silin.

# conftest.py, complete: pip install pytest capskip==1.3.0 requests
import functools
import os
import socket
import types

import capskip.api
import pytest
import requests
from capskip import AsyncCapSkip, CapSkip

from tokens import fresh_token

# The SDK reads no environment variables, so this file reads them.
HOST = os.environ.get("CAPSKIP_HOST", "127.0.0.1")
PORT = int(os.environ.get("CAPSKIP_PORT", "8080"))
API_KEY = os.environ.get("CAPSKIP_API_KEY", "capskip")

# capskip 1.3.0 sets no HTTP timeout. Give every SDK call 5 s to connect
# and 30 s to answer. Pin capskip==1.3.0: this swaps a name inside it.
if hasattr(capskip.api, "requests"):
    capskip.api.requests = types.SimpleNamespace(
        RequestException=requests.RequestException,
        post=functools.partial(requests.post, timeout=(5, 30)),
        get=functools.partial(requests.get, timeout=(5, 30)),
    )


def pytest_configure(config):
    # Register the marker so --strict-markers accepts it.
    config.addinivalue_line(
        "markers", "captcha: needs a running CapSkip and solves a real CAPTCHA"
    )


@pytest.fixture(scope="session")
def capskip_client():
    # No ping endpoint, so a plain TCP connect. Use the IP, not "localhost".
    try:
        socket.create_connection((HOST, PORT), timeout=3).close()
    except OSError as exc:
        pytest.skip(f"CapSkip is not running on {HOST}:{PORT}: {exc}")
    return CapSkip(apiKey=API_KEY, host=HOST, port=PORT, recaptchaTimeout=90)


@pytest.fixture
def solve_recaptcha(capskip_client):
    # A factory: every call is a new solve, and a token works once.
    def solve(sitekey, url):
        return fresh_token(capskip_client, sitekey, url)

    return solve


@pytest.fixture(scope="session")
def async_capskip_client(capskip_client):
    return AsyncCapSkip(apiKey=API_KEY, host=HOST, port=PORT, recaptchaTimeout=90)

pytest CAPTCHA testlerinde sık görülen hatalar

GördüğünüzNedenDüzeltme
SKIPPED: CapSkip is not running on 127.0.0.1:8080Orada hiçbir şey dinlemiyor ya da CAPSKIP_HOST http:// içeriyorCapSkip’i başlatın, bağlantı ayarlarını kontrol edin, http:// olmadan bir host verin
no attribute SIGALRM diyen INTERNALERRORsignal zaman aşımı yöntemi Windows’ta yokturthread yöntemini kullanın
Bir yığın dökümü (stack dump), özet yok, çıkış kodu 1thread zaman aşımı tetiklendi ve çalıştırmayı sonlandırdıSDK’nın önce başarısız olmasını sağlayın: recaptchaTimeout ve istek yamasıyla
worker gw1 crashed while runningthread zaman aşımı bir xdist worker’ını öldürdüAynı düzeltme; ayrıca takılan testi arayın
‘captcah’ not found in `markers` configuration optionStrict seçeneği açıkken yanlış yazılmış bir markerYazımı düzeltin
ModuleNotFoundError: No module named ‘tokens’pytest.exe proje klasörünü yola eklemezpython -m pytest çalıştırın
Failed: async def functions are not natively supportedasyncio marker’ı olmayan bir async testMarker’ı ekleyin
Giriş gönderiminde 403requests Referer göndermedi ve site onu denetliyorReferer başlığını verin
İkinci test başarısız olur, ilki geçerİki test aynı token’ı gönderdiÇözümü her testin içinde yapın
NetworkException: Read timed out (read timeout=30)CapSkip gönderimi aldı ve sessizleştiCapSkip’i kontrol edin, sonra yeniden çalıştırın
Yaklaşık 333 saniye sonra TimeoutExceptionSessiz bir yoklama, 300 saniyelik varsayılan sınırlarecaptchaTimeout değerini 90 yapın
Her çözüm daha yavaş ve ilk denetim 2 saniye daha uzun sürüyorCAPSKIP_HOST localhost127.0.0.1 kullanın
AttributeError: ‘NoneType’ object has no attribute ‘group’LOGIN_URL adresindeki sayfada CSRF token’ı ya da sitekey yok, yani LOGIN_URL hâlâ örnek adresi tutuyorLOGIN_URL değerini kendi giriş sayfanıza ayarlayın

FAQ

Barındırılan bir CI runner’ı bu testleri çalıştırabilir mi?

Barındırılan bir runner başkasının makinesidir, bu yüzden bilgisayarınızdaki 127.0.0.1’e ulaşamaz. Server modundaki CapSkip için genel bir adrese ihtiyaç duyar. Anahtar o zaman düz HTTP üzerinden açık metin olarak taşınır ve bir engelleme kuralı sürekli değişen runner adreslerini listeleyemez. Kendi ağınızdaki kendi barındırdığınız (self-hosted) bir runner’ı ya da bir VPN’i tercih edin ve API anahtarını CI sisteminin gizli bilgi deposunda tutun. Bu rehber için barındırılan hiçbir runner test edilmedi. Yalnızca değişkenler okundu.

Her build CAPTCHA testlerini çalıştırmalı mı?

Hayır. Her biri saniyeler sürer, çalışan bir CapSkip gerektirir ve gerçek bir staging sitesine dokunur. Marker, sıradan bir build’in hızlı test paketini, zamanlanmış bir işin ise canlı olanı çalıştırmasını sağlar. Canlı olanı bir sürümden önce ya da geceleri çalıştırın.

Bu, standart unittest modülüyle çalışır mı?

CapSkip çağrıları her framework’te aynıdır. Yukarıdaki oturum istemcisi, atlama, marker ve paralel ayarlar pytest özellikleridir ve unittest bu rehber için çalıştırılmadı. Kendi fixture’ları, atlamaları ve zaman aşımları için runner’ınızın belgelerine bakın.

Selenium ya da Playwright gerekir mi?

Bu kurulum için hayır. Formu requests ile gönderir, dolayısıyla sunucunun denetlediği bir giriş akışını test eder. CAPTCHA yalnızca gerçek bir tarayıcı oturumunda çalışıyorsa testten bir tarayıcı sürün ve token’ı sayfaya verin. Çözücü çağrıları aynı kalır.

Kısa özet

Bir pytest CAPTCHA test paketi, oturum başına bir CapSkip istemcisine ve her testin içinde yeni bir çözüme ihtiyaç duyar. CapSkip kapalıyken atlayın, canlı testleri işaretleyin ve açık bir worker sayısını CapSkip’in 10 thread’inin altında tutun. Windows’ta SDK’nın kendi sınırlarını pytest-timeout sınırının çok altında ayarlayın; böylece takılan bir çözüm çalıştırmayı sonlandırmak yerine tek bir testi başarısız kılar.

Gece çalışan ve birkaç yüz kez çözüm yapan bir test paketi çabuk birikir. Bunu kendi Windows makinenizde çalışan bir captcha çözücü üzerinden yürütmek, bu çözümlerin asla bir faturaya dönüşmemesi demektir.