Cara Memecahkan CAPTCHA di pytest Tanpa Membuat Run Menggantung

Untuk memecahkan CAPTCHA di pytest, buat satu klien CapSkip per sesi di conftest.py dan pecahkan di dalam setiap test, karena token hanya bisa dipakai sekali. Lewati test CAPTCHA saat CapSkip tidak berjalan. Lalu beri SDK timeout-nya sendiri. Di Windows, pytest-timeout hanya bisa memakai metode thread, dan saat metode itu terpicu, seluruh run berakhir. SDK Python tidak menetapkan timeout HTTP, jadi CapSkip yang diam akan menahan sebuah test tanpa error apa pun. CapSkip berjalan di PC Windows Anda sendiri, sehingga suite yang memecahkan CAPTCHA ratusan kali per malam tidak memiliki biaya per pemecahan. Penyiapan CAPTCHA pytest ini membahas setiap jebakan itu, ditambah run paralel dengan xdist dan test async.
Apa yang Anda butuhkan
- CapSkip yang berjalan di PC Windows, dengan pemecah reCAPTCHA diaktifkan. Lihat panduan penyiapan yang menjelaskan cara menjalankan CapSkip untuk pertama kali.
- Python 3.10 atau lebih baru. Paket capskip 1.3.0 dan pytest 9 sama-sama membutuhkannya. Semua yang ada di bawah dijalankan di Windows 11 dengan Python 3.14.0, pytest 9.1.1, pytest-timeout 2.4.0, pytest-xdist 3.8.0, pytest-asyncio 1.4.0, dan requests 2.34.2. Waktu untuk koneksi yang ditolak ternyata sama di 3.13.7.
- Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
- Situs staging milik Anda sendiri, dengan kotak centang reCAPTCHA v2 di sebuah form login. Contohnya login ke form bergaya Django yang membawa token CSRF. Ubah nama field agar sesuai dengan milik Anda.
- Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga test di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
Pengukuran di bawah berasal dari mock CapSkip (pengganti CapSkip yang dibuat khusus untuk pengujian) dan halaman login mock di PC yang sama, dipakai bersama pytest yang asli dan SDK yang asli. Perilaku pytest dan perilaku SDK juga berlaku di situs Anda sendiri. Waktu pemecahan, batas thread, serta aturan menerima dan menolak pada halaman login adalah milik mock itu sendiri.
Langkah 1: buat proyek
Taruh semuanya dalam satu folder proyek: conftest.py, tokens.py, dan file test. Gunakan virtual environment. Setiap perintah di bawah menjalankan Python milik virtual environment itu lewat path-nya, jadi Anda tidak perlu mengaktifkannya. Jalankan pytest sebagai python -m pytest. Bentuk itu menambahkan folder saat ini ke path import. Jika Anda memindahkan conftest.py ke folder tests, biarkan tokens.py di folder proyek. Dalam kasus itu pytest.exe gagal dengan ModuleNotFoundError: No module named ‘tokens’, sedangkan python -m pytest berfungsi.
# Windows PowerShell, in your project folder python -m venv .venv .venv\Scripts\python -m pip install pytest "capskip==1.3.0" requests
Langkah 2: satu klien CapSkip untuk seluruh eksekusi
Simpan ini sebagai conftest.py. File ini membaca pengaturan CapSkip dari environment, karena SDK tidak membacanya sendiri, dan nilai default membuatnya berjalan tanpa penyiapan. CAPSKIP_API_KEY hanya berpengaruh jika Anda mengaktifkan validasi API key di CapSkip. Fixture membangun satu klien untuk seluruh sesi. Fixture juga mencari tahu apakah CapSkip sedang berjalan.
# 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 tidak memiliki endpoint ping, jadi koneksi TCP biasa dipakai untuk menanyakan apakah ada yang mendengarkan, dan koneksi itu tidak mengirim job pemecahan. Jika tidak ada, fixture melewati test alih-alih gagal. Saat CapSkip ditutup, koneksi ke 127.0.0.1 yang ditolak memakan 2.05 detik di Windows, dan sekitar 4.1 detik ke nama localhost. Hasil skip di-cache, jadi satu sesi penuh membayar biaya itu sekali saja: empat test CAPTCHA di dua file memakan 2.05 detik secara total.
Gunakan IP-nya. Nama localhost punya biaya kedua bahkan saat CapSkip sedang berjalan. Pada listener yang hanya IPv4, dua test login memakan 10.3 detik dengan localhost dan 3.75 detik dengan 127.0.0.1. Itu sekitar 2 detik sekali untuk pemeriksaan koneksi, ditambah 1 sampai 2.5 detik lagi untuk setiap pemecahan.
Langkah 3: helper yang memecahkan sekali, dan mencoba ulang sekali
Simpan ini sebagai tokens.py. Sebagian pemecahan gagal, dan percobaan baru bisa berhasil. SDK memunculkan ApiException dengan pesan ERROR_CAPTCHA_UNSOLVABLE, kecuali Anda menyetel teks Error Replacement di CapSkip. Dalam kasus itu pesannya berisi teks tersebut, dan pemeriksaan di bawah tidak pernah cocok. Helper hanya mencoba ulang kegagalan itu. Pengiriman yang ditolak CapSkip, seperti ERROR_GOOGLEKEY atau ERROR_PAGEURL, langsung memunculkan exception. Sitekey yang ditolak Google juga terbaca sebagai UNSOLVABLE, jadi dicoba dua kali sebelum gagal. Lihat ERROR_CAPTCHA_UNSOLVABLE untuk arti error tersebut.
# 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):
raiseLangkah 4: sebuah fixture per test, dan sebuah marker
Tambahkan blok berikutnya di akhir conftest.py. Blok ini berupa factory, bukan token. Setiap pemanggilannya adalah pemecahan baru, dan setiap test meminta miliknya sendiri, karena token reCAPTCHA hanya diterima sekali. Python mengizinkan import di tengah file, jadi baris import bisa tetap berada di tempat yang ditentukan blok itu.
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 solveLalu tambahkan hook marker di bawah ini ke conftest.py. Hook ini mendaftarkan marker bernama captcha, sehingga Anda bisa menjalankan test cepat tanpa CapSkip dan test live dengan CapSkip. Dengan opsi strict markers aktif, yang dinyalakan Langkah 6, marker apa pun yang tidak Anda daftarkan menjadi error. Mendaftarkan captcha menjaga marker yang asli tetap berfungsi dan mengubah salah ketik menjadi error.
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"
)Langkah 5: tulis test login
Simpan dua blok berikutnya sebagai satu file, test_login.py. Blok pertama ini milik Anda, bukan milik CapSkip. Blok ini membuka halaman login, membaca token CSRF dan sitekey, lalu mengirim form. requests tidak mengirim Referer pada form post. Situs yang ketat seperti Django melalui HTTPS menolak post tanpa Referer, dan mock melakukan hal yang sama dengan 403. Karena itu helper menyetelnya. Sesuaikan kedua pola dan nama field dengan markup Anda, lalu isi email dan kata sandi akun test ke post_form.
# 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,
})Test memecahkan CAPTCHA paling akhir lalu langsung mengirim form, karena token kedaluwarsa setelah sekitar dua menit. Lihat panduan kedaluwarsa token reCAPTCHA yang menjelaskan sisi itu. Ubah Welcome, 403, dan teks error sesuai yang ditampilkan situs Anda sendiri. Username yang tetap akan bentrok pada run malam kedua, jadi pakai yang baru setiap kali jika form Anda membuat akun.
@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.textLangkah 6: jalankan test cepat, lalu test live
Baris pertama mengarahkan test ke halaman login Anda. Baris kedua membuat marker yang salah eja menjadi error. Baris itu menyetel variabel lingkungan yang dibaca pytest untuk opsi tambahan, dan nilainya bertahan selama sisa sesi PowerShell tersebut. Baris ketiga menjalankan semuanya kecuali test CAPTCHA. Baris keempat hanya menjalankan test live. Opsi skip mencetak alasan sebuah test dilewati, dan opsi durations mencantumkan test yang paling lambat.
# 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
Pada tahap ini run cepat tidak menemukan apa pun untuk dijalankan, dan pytest keluar dengan kode 5. Test-nya baru ada setelah Anda menyimpan test_unit.py dari bagian unit test di bawah. Setelah itu run tidak membutuhkan apa pun dari pemecah CAPTCHA: keempat test-nya lulus dalam 0.02 detik dengan CapSkip ditutup. Run live dilewati dengan baris seperti ini, yang menunjuk ke test dan bukan ke conftest.py:
- 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
Suite yang dilewati keluar dengan kode 0, dan pytest 9.1.1 tidak punya opsi yang mengubah skip menjadi kegagalan. Jika build Anda harus memakai CapSkip, tulis junit file dengan opsi junitxml dan gagalkan build saat jumlah skip-nya di atas nol. Mengetik captcah, bukan captcha, pada sebuah test, dengan opsi strict aktif, menghentikan collection dengan exit code 2 dan pesan ‘captcah’ not found in `markers` configuration option. Tanpa opsi strict, salah ketik itu hanya menjadi peringatan, dan test tersebut akan berjalan di suite cepat.
Pecahkan di dalam setiap test
Jangan memecahkan sekali di fixture lalu membagikan token itu. Dalam pengujian, fixture dengan scope module memecahkan sekali dan dua test mengirim token yang sama. Yang pertama diterima. Yang kedua gagal dengan pesan dari mock, reCAPTCHA failed: token already used. Situs sungguhan menyatakannya dengan kata-kata berbeda. Test mana yang kalah bergantung pada urutan, sehingga kegagalannya berpindah-pindah dan tampak tidak stabil (flaky). Kegagalan ini lebih buruk di bawah xdist, seperti ditunjukkan bagian paralel. Berikut kesalahan itu secara utuh, supaya Anda bisa mengenalinya. Hapus file itu setelah Anda melihatnya gagal, atau file itu akan membuat run berikutnya gagal atau lulus secara acak.
# 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 alreadySatu pemecahan per test memakan beberapa detik untuk masing-masing. Test yang tidak membutuhkan CAPTCHA itu sendiri tidak perlu meminta fixture-nya. Pemecah tiruan di bawah mencakup logika di sekitarnya.
Membuat pemecahan yang menggantung gagal, bukan menghentikan run
Bagian ini berbeda di Windows. Plugin pytest-timeout memiliki dua metode. Metode signal membutuhkan SIGALRM, yang tidak dimiliki Windows, sehingga metode itu tidak tersedia di sana. Memintanya berakhir dengan INTERNALERROR dan AttributeError: module ‘signal’ has no attribute ‘SIGALRM’. Metode thread adalah default, dan satu-satunya yang berfungsi. Berikut yang terjadi ketika sebuah test menggantung, dengan batas 10 detik dan empat test dalam run:
- Metode ini mencetak stack thread utama, sekitar 100 baris, dan mengakhiri proses dengan exit code 1 pada 12.5 detik.
- Test yang menggantung tidak mendapat baris PASSED atau FAILED.
- Tidak ada ringkasan yang dicetak dan tidak ada junit file yang ditulis.
- Dua test setelahnya tidak pernah berjalan.
Dengan xdist kerusakannya lebih kecil. Hanya worker yang menjalankan test itu yang mati. Test dilaporkan sebagai FAILED dengan pesan worker gw1 crashed while running, worker pengganti dimulai, dan run selesai dengan ringkasan serta exit code 1.
Jadi timeout thread adalah jalan terakhir, bukan timeout test. Anda ingin SDK gagal lebih dulu, dengan exception biasa, sehingga test gagal dan run terus berjalan. SDK Python 1.3.0 mempersulit hal itu. SDK memanggil requests tanpa timeout, jadi CapSkip yang menerima pemanggilan tetapi tidak pernah menjawab membuatnya menggantung. Tanpa perubahan apa pun, pengiriman yang diam masih terblokir saat kami menghentikannya pada 45 detik. recaptchaTimeout milik SDK hanya diperiksa di antara polling, dan polling yang menggantung dianggap belum siap.
Perbaikannya adalah mengganti dua fungsi request yang dipakai SDK dengan fungsi yang memiliki timeout. Tambahkan blok berikutnya ke conftest.py. Blok ini menyentuh bagian dalam SDK, jadi kunci versi capskip ke 1.3.0. Baris hasattr membuatnya tidak melakukan apa pun jika versi yang lebih baru berhenti memakai requests. Perbaikan ini tidak mencakup pengunduhan CAPTCHA gambar yang diberikan sebagai URL, karena SDK melakukannya dengan pemanggilannya sendiri.
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)),
)Dengan patch itu, CapSkip yang tidak pernah menjawab pengiriman memunculkan NetworkException dengan Read timed out. (read timeout=30) pada 30.0 detik. Test pytest gagal pada 30.8 detik, dan run tetap utuh. Polling yang diam berbeda. SDK menelannya sebagai belum siap, dan recaptchaTimeout default 300 detik berakhir dengan TimeoutException pada 333.0 detik. Itulah sebabnya Langkah 2 meneruskan recaptchaTimeout=90. Dengan 90, test gagal pada 92.6 detik.
Tenggat bisa terlampaui hingga sekitar 35 detik. Tenggat itu diperiksa di antara polling, satu polling bisa memakan penuh 30 detiknya, dan SDK menunggu hingga 5 detik sebelum memeriksa. Dengan batas 45, kegagalan terjadi pada 60.8 detik, dan dengan 20 pada 30.3 detik. Jadi kasus terburuk untuk satu percobaan dengan angka-angka ini sekitar 125 detik: 90 untuk polling, ditambah satu polling yang macet. Helper dari Langkah 3 melakukan hingga dua percobaan, jadi kasus terburuknya sekitar 250 detik. Setel batas pytest-timeout di atas angka itu. Blok berikutnya menginstal plugin dan menyetel 300 detik. Variabel lingkungan itu menggantikan yang dari Langkah 6, jadi ia mengulang opsi strict. Sebagai gantinya, Anda bisa menaruh kedua opsi yang sama di file ini pytest sebagai timeout dan timeout_method. Variabelnya hanya lebih mudah ditempel.
# 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"
Menjalankan test secara paralel dengan xdist
Pemecahan itu lambat dan sebagian besar berupa menunggu, jadi xdist membantu. Setiap worker xdist adalah proses terpisah. Karena itu fixture sesi berjalan sekali per worker, dan setiap worker membangun klien miliknya sendiri. Dengan empat worker dan delapan test, run menampilkan empat process ID dan empat objek klien. Tanpa xdist, masing-masing hanya satu. Test menulis workers-0.log sampai workers-7.log, dan Get-Content workers-*.log menampilkannya. Satu klien per worker tidak masalah, karena klien tidak menyimpan apa pun yang perlu dibagikan antar worker.
# 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)Tentukan jumlah worker sendiri. Pengaturan auto berarti jumlah CPU, bukan jumlah yang bisa dipecahkan CapSkip sekaligus. Pengaturan itu menghasilkan 24 worker di PC uji. Empat unit test memakan sekitar 4 detik dengannya dan sekitar 1.1 detik dengan empat worker, karena biaya start-up worker. Batas milik CapSkip sendiri adalah pengaturan reCAPTCHA Max. Threads, yang default-nya 10. Tabel ini berasal dari 24 login terhadap mock dengan batas itu dan pemecahan 5 detik.
| Worker | Waktu eksekusi | Pemecahan sekaligus |
|---|---|---|
| 1 | 189.6 s | 1 |
| 4 | 49.1 s | 4 |
| 10 | 26.1 s | 10 |
| 12 | 24.1 s | 10 |
| 24 | 23.1 s | 10 |
Di atas 10 worker, peningkatannya kecil: 26.1 detik dengan 10 worker dan 23.1 dengan 24. Job tambahan menunggu thread yang kosong di dalam CapSkip, dan job yang menunggu melewati Wait Timeout akan gagal. Dalam pengujian berskala kecil, delapan test berjalan sekaligus terhadap batas 2, batas tunggu 6 detik, dan pemecahan 4 detik. Empat job gagal dengan ERROR_CAPTCHA_UNSOLVABLE. Percobaan ulang di tokens.py mengantrekan job itu lagi, dan kedelapan test lulus dalam 22.3 detik. Itulah alasan kedua untuk percobaan ulang itu.
Setiap test memakan sekitar 7.9 detik untuk pemecahan 5 detik. SDK melakukan polling pada 0, 0.25, 0.75, 1.75, 3.75, dan 7.75 detik, sehingga baru menyadari pemecahan yang selesai pada polling berikutnya. Instalasi xdist dan perintahnya ada di bawah.
# 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
Satu jebakan lagi. Di bawah xdist, kesalahan token bersama dari bagian sebelumnya bisa lulus. Token dengan scope module dipecahkan sekali per worker, jadi dua test hanya bentrok jika kebetulan berada di worker yang sama. Test yang lulus di sebagian run dan gagal di run lain adalah petunjuk untuk mencari token bersama.
Test async dengan AsyncCapSkip
SDK Python memiliki klien async yang sungguhan, AsyncCapSkip. Klien ini dibangun di atas httpx. Tambahkan fixture ini ke conftest.py. Fixture ini meminta capskip_client, jadi pemeriksaan skip tetap berlaku. Klien async tidak memegang event loop, jadi fixture biasa sudah cukup dan test berjalan pada loop default dengan scope fungsi.
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 berjalan dalam mode strict, dan pytest 9 menggagalkan test async yang tidak memiliki marker dengan Failed: async def functions are not natively supported. Simpan blok berikutnya sebagai test_async.py. Ubah YOUR_SITEKEY menjadi sitekey halaman Anda.
# 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 tokensJangan berharap skalanya naik secara lurus. Terhadap mock yang memecahkan dalam 1.5 detik, menjalankan 1, 5, 10, dan 25 pemecahan dengan gather memakan 2.6, 2.5, 4.6, dan 9.8 detik. Klien membuka koneksi httpx baru untuk setiap pemanggilan, yang membuat event loop sibuk sejenak setiap kali. Patch dari bagian timeout tidak berlaku untuk klien ini. Batasnya adalah default httpx, 5 detik per request, bukan per pemecahan, jadi pemecahan 12 detik tetap berhasil dan selesai pada 14.1 detik. Pengiriman yang diam memunculkan NetworkException dengan pesan kosong pada 5.2 detik. Polling yang diam berakhir dengan TimeoutException pada 24.7 detik dengan batas 20. Untuk pembahasan lebih lanjut tentang konkurensi di Python, lihat memecahkan CAPTCHA secara paralel dengan asyncio.
Unit test yang tidak membutuhkan pemecah CAPTCHA
Sebagian besar test Anda tidak seharusnya membutuhkan CapSkip. Test untuk kode Anda sendiri yang memanggil pemecah CAPTCHA bisa memakai pemecah tiruan. Hasilnya harus sama persis dengan yang asli. SDK mengembalikan dict biasa berisi captchaId dan code, jadi pemecah tiruan juga begitu. Test terakhir menunjukkan cara lain: bangun klien asli, yang tidak membuka koneksi sampai Anda memecahkan, dan ganti satu method dengan monkeypatch. Keempatnya lulus dengan CapSkip ditutup. Simpan blok itu sebagai test_unit.py.
# 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"Menjalankan test terhadap CapSkip di mesin lain
Test dan pemecah CAPTCHA tidak harus berada di PC yang sama. Alihkan CapSkip ke mode Server di pengaturan koneksi, dan ia akan mendengarkan di alamat jaringan atau IP publik Anda. IP publik statis direkomendasikan untuk akses jarak jauh yang stabil. Lalu setel ketiga variabel, dan suite yang sama berjalan tanpa perubahan. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.
# 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
Perhatikan dua hal. SDK berbicara dengan CapSkip lewat HTTP biasa, sehingga API key dikirim sebagai teks polos. Jaga agar tautan itu berada di jaringan pribadi atau VPN, dan aktifkan validasi API key. Lalu CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai. Untuk membatasi akses hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Penyiapan jarak jauh itu tidak dijalankan untuk panduan ini. Hanya variabelnya yang dibaca.
Seluruh conftest.py
Berikut conftest.py dari Langkah 2 dan 4 ditambah patch timeout dan fixture async, dalam satu file. File ini menggantikan blok-blok yang Anda tambahkan satu per satu, jadi hapus blok-blok itu lebih dulu.
# 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)Kesalahan umum pada test CAPTCHA pytest
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| SKIPPED: CapSkip is not running on 127.0.0.1:8080 | Tidak ada yang mendengarkan di sana, atau CAPSKIP_HOST menyertakan http:// | Jalankan CapSkip, periksa pengaturan koneksi, berikan host tanpa http:// |
| INTERNALERROR dengan no attribute SIGALRM | Metode timeout signal tidak ada di Windows | Gunakan metode thread |
| Stack dump, tanpa ringkasan, exit code 1 | Timeout thread terpicu dan mengakhiri run | Buat SDK gagal lebih dulu, dengan recaptchaTimeout dan patch request |
| worker gw1 crashed while running | Timeout thread mematikan satu worker xdist | Perbaikan yang sama, dan cari test yang menggantung |
| ‘captcah’ not found in `markers` configuration option | Marker yang salah eja dengan opsi strict aktif | Perbaiki ejaannya |
| ModuleNotFoundError: No module named ‘tokens’ | pytest.exe tidak menaruh folder proyek di path | Jalankan python -m pytest |
| Failed: async def functions are not natively supported | Test async tanpa marker asyncio | Tambahkan marker |
| 403 pada post login | requests tidak mengirim Referer, dan situs memeriksanya | Berikan header Referer |
| Test kedua gagal, yang pertama lulus | Dua test mengirim satu token yang sama | Pecahkan di dalam setiap test |
| NetworkException: Read timed out (read timeout=30) | CapSkip menerima pengiriman lalu diam | Periksa CapSkip, lalu jalankan ulang |
| TimeoutException setelah sekitar 333 detik | Polling yang diam, dengan batas default 300 detik | Setel recaptchaTimeout ke 90 |
| Setiap pemecahan lebih lambat, dan pemeriksaan pertama memakan 2 detik lebih lama | CAPSKIP_HOST adalah localhost | Gunakan 127.0.0.1 |
| AttributeError: ‘NoneType’ object has no attribute ‘group’ | Halaman di LOGIN_URL tidak memiliki token CSRF atau sitekey, karena LOGIN_URL masih berisi alamat contoh | Setel LOGIN_URL ke halaman login Anda sendiri |
FAQ
Bisakah runner CI hosted menjalankan test ini?
Runner hosted adalah mesin milik pihak lain, jadi tidak bisa menjangkau 127.0.0.1 di PC Anda. Runner itu membutuhkan alamat publik untuk CapSkip dalam mode Server. Key lalu dikirim sebagai teks polos lewat HTTP biasa, dan aturan blokir tidak bisa mencantumkan alamat runner yang terus berubah. Lebih baik pakai runner self-hosted di jaringan Anda sendiri, atau VPN, dan simpan API key di secret store milik sistem CI. Tidak ada runner hosted yang diuji untuk panduan ini. Hanya variabelnya yang dibaca.
Haruskah setiap build menjalankan test CAPTCHA?
Tidak. Masing-masing memakan beberapa detik, membutuhkan CapSkip yang sedang berjalan, dan menyentuh situs staging yang sungguhan. Marker memungkinkan build biasa menjalankan suite cepat dan job terjadwal menjalankan suite live. Jalankan suite live sebelum rilis, atau setiap malam.
Apakah ini berfungsi dengan modul unittest standar?
Pemanggilan CapSkip sama di framework mana pun. Klien sesi, skip, marker, dan pengaturan paralel di atas adalah fitur pytest, dan unittest tidak dijalankan untuk panduan ini. Periksa dokumentasi runner Anda untuk fixture, skip, dan timeout miliknya sendiri.
Apakah saya membutuhkan Selenium atau Playwright?
Tidak untuk penyiapan ini. Penyiapan ini mengirim form dengan requests, jadi yang diuji adalah alur login yang diperiksa oleh server. Jika CAPTCHA hanya berfungsi di dalam sesi browser sungguhan, jalankan browser dari test dan serahkan token ke halaman. Pemanggilan pemecah CAPTCHA tetap sama.
Versi singkatnya
Suite CAPTCHA pytest membutuhkan satu klien CapSkip per sesi dan pemecahan baru di dalam setiap test. Lakukan skip saat CapSkip ditutup, beri marker pada test live, dan pertahankan jumlah worker yang eksplisit di bawah 10 thread milik CapSkip. Di Windows, setel batas milik SDK sendiri jauh di bawah batas pytest-timeout, agar pemecahan yang menggantung menggagalkan satu test, bukan mengakhiri run.
- Setiap tipe CAPTCHA yang ditangani paket Python: halaman pemecah CAPTCHA Python.
- reCAPTCHA kotak centang di skrip Python biasa: memecahkan reCAPTCHA v2 di Python.
- Cara kerja pemecah kotak centang: halaman pemecah reCAPTCHA v2.
Test suite malam yang memecahkan CAPTCHA beberapa ratus kali jumlahnya cepat menumpuk. Jika dijalankan melalui sebuah pemecah captcha lokal di mesin Windows Anda sendiri, pemecahan itu tidak pernah berubah menjadi tagihan.
