CAPTCHA-solving code touches external APIs, proxies, and browser interactions — it needs tests. But hitting the CaptchaAI API on every test run is slow and costs credits. Pytest fixtures let you mock the API for unit tests and run live integration tests only when needed.
This tutorial builds a reusable fixture library for CaptchaAI testing.
Project structure
tests/
├── conftest.py # Shared fixtures
├── test_solver.py # Unit tests (mocked)
├── test_integration.py # Live API tests (optional)
└── captcha_client.py # Your CaptchaAI client
What you need
| Requirement | Details |
|---|---|
| Python 3.8+ | With pytest, requests, responses |
| CaptchaAI API key | For integration tests only |
pip install pytest requests responses
CaptchaAI client module
# captcha_client.py
import requests
import time
class CaptchaClient:
BASE_URL = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def submit(self, params):
"""Submit a CAPTCHA task and return the task ID."""
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE_URL}/in.php", data=params)
resp.raise_for_status()
data = resp.json()
if data.get("status") != 1:
raise RuntimeError(f"Submit failed: {data.get('request')}")
return data["request"]
def poll(self, task_id, timeout=120, interval=5):
"""Poll for the result of a submitted task."""
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(f"{self.BASE_URL}/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1
})
resp.raise_for_status()
data = resp.json()
if data.get("status") == 1:
return data["request"]
if data.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(f"Solve failed: {data['request']}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} timed out after {timeout}s")
def solve(self, params, timeout=120):
"""Submit and poll in one call."""
task_id = self.submit(params)
return self.poll(task_id, timeout=timeout)
def balance(self):
"""Check account balance."""
resp = requests.get(f"{self.BASE_URL}/res.php", params={
"key": self.api_key, "action": "getbalance", "json": 1
})
resp.raise_for_status()
return float(resp.json().get("request", 0))
Shared fixtures — conftest.py
# tests/conftest.py
import os
import pytest
import responses
from captcha_client import CaptchaClient
@pytest.fixture
def api_key():
"""Return test API key — use env var for live tests."""
return os.getenv("CAPTCHAAI_API_KEY", "TEST_KEY_000000")
@pytest.fixture
def client(api_key):
"""CaptchaClient instance."""
return CaptchaClient(api_key)
@pytest.fixture
def mock_api():
"""Activate responses mock for CaptchaAI endpoints."""
with responses.RequestsMock() as rsps:
yield rsps
@pytest.fixture
def mock_successful_solve(mock_api):
"""Mock a successful submit + poll cycle."""
mock_api.add(
responses.POST,
"https://ocr.captchaai.com/in.php",
json={"status": 1, "request": "12345"},
status=200
)
mock_api.add(
responses.GET,
"https://ocr.captchaai.com/res.php",
json={"status": 1, "request": "SOLVED_TOKEN_abc123"},
status=200
)
return mock_api
@pytest.fixture
def mock_pending_then_solved(mock_api):
"""Mock a submit, then pending, then solved cycle."""
mock_api.add(
responses.POST,
"https://ocr.captchaai.com/in.php",
json={"status": 1, "request": "12345"},
status=200
)
# First poll: not ready
mock_api.add(
responses.GET,
"https://ocr.captchaai.com/res.php",
json={"status": 0, "request": "CAPCHA_NOT_READY"},
status=200
)
# Second poll: solved
mock_api.add(
responses.GET,
"https://ocr.captchaai.com/res.php",
json={"status": 1, "request": "SOLVED_TOKEN_xyz789"},
status=200
)
return mock_api
@pytest.fixture
def mock_submit_error(mock_api):
"""Mock a submit error (e.g., wrong API key)."""
mock_api.add(
responses.POST,
"https://ocr.captchaai.com/in.php",
json={"status": 0, "request": "ERROR_WRONG_USER_KEY"},
status=200
)
return mock_api
@pytest.fixture
def recaptcha_params():
"""Standard reCAPTCHA v2 parameters."""
return {
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": "https://example.com/login"
}
@pytest.fixture
def turnstile_params():
"""Standard Cloudflare Turnstile parameters."""
return {
"method": "turnstile",
"sitekey": "0x4AAAAAAADnPIDROz1234",
"pageurl": "https://example.com/protected"
}
Unit tests — mocked API
# tests/test_solver.py
import pytest
def test_solve_returns_token(client, mock_successful_solve, recaptcha_params):
"""Successful solve returns a token string."""
token = client.solve(recaptcha_params, timeout=10)
assert token == "SOLVED_TOKEN_abc123"
assert len(token) > 0
def test_solve_handles_pending(client, mock_pending_then_solved, recaptcha_params):
"""Client retries when task is not ready."""
token = client.solve(recaptcha_params, timeout=30)
assert token == "SOLVED_TOKEN_xyz789"
def test_submit_error_raises(client, mock_submit_error, recaptcha_params):
"""Submit errors raise RuntimeError."""
with pytest.raises(RuntimeError, match="ERROR_WRONG_USER_KEY"):
client.solve(recaptcha_params)
def test_submit_sends_correct_params(client, mock_successful_solve, recaptcha_params):
"""Verify the correct parameters are sent to in.php."""
client.solve(recaptcha_params, timeout=10)
body = mock_successful_solve.calls[0].request.body
assert "userrecaptcha" in body
assert "googlekey" in body
def test_balance_returns_float(client, mock_api):
"""Balance check returns a float value."""
mock_api.add(
"GET",
"https://ocr.captchaai.com/res.php",
json={"status": 1, "request": "12.50"},
status=200
)
balance = client.balance()
assert isinstance(balance, float)
assert balance == 12.50
Integration tests — live API
# tests/test_integration.py
import os
import pytest
pytestmark = pytest.mark.skipif(
not os.getenv("CAPTCHAAI_API_KEY"),
reason="CAPTCHAAI_API_KEY not set — skipping live tests"
)
def test_live_balance(client):
"""Verify balance check works against the real API."""
balance = client.balance()
assert isinstance(balance, float)
assert balance >= 0
def test_live_submit_recaptcha(client, recaptcha_params):
"""Submit a reCAPTCHA task to the live API."""
task_id = client.submit(recaptcha_params)
assert task_id.isdigit()
Run mocked tests:
pytest tests/test_solver.py -v
Run live tests:
CAPTCHAAI_API_KEY=your_key pytest tests/test_integration.py -v
Expected output (mocked):
tests/test_solver.py::test_solve_returns_token PASSED
tests/test_solver.py::test_solve_handles_pending PASSED
tests/test_solver.py::test_submit_error_raises PASSED
tests/test_solver.py::test_submit_sends_correct_params PASSED
tests/test_solver.py::test_balance_returns_float PASSED
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
ConnectionError in mocked tests |
responses not activated |
Use the mock_api fixture or @responses.activate |
| Live tests skipped | CAPTCHAAI_API_KEY not set |
Export the env variable before running |
TimeoutError in live tests |
Solve took too long | Increase timeout parameter |
| Mock returns wrong response | Response order matters | responses serves mocks in FIFO order |
FAQ
Should I mock or use live tests?
Mock for unit tests (fast, free, deterministic). Use live tests only for smoke tests in CI or integration validation. Mark live tests with pytest.mark.skipif so they don't run unless the API key is set.
How do I test error handling?
Create fixtures for each error code — ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE, ERROR_NO_SLOT_AVAILABLE. Assert that your client raises the correct exception.
Can I use these fixtures for other CAPTCHA types?
Yes. Add parameter fixtures for each CAPTCHA type (GeeTest, image/OCR, Cloudflare Challenge). The solver logic is the same — only the submit parameters change.
Get your CaptchaAI API key
Start testing your CAPTCHA-solving workflows at captchaai.com. These fixtures work with every CAPTCHA type CaptchaAI supports.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.