Chapter 15 · pytest Integration Testing
Chapter goals: Master pytest-httpx plugin and pytest-asyncio to write reproducible, parallelizable HTTP client tests.
15.1 Why HTTP Tests Need Dedicated Tools
Making real HTTP requests for testing has three problems:
- Slow: every test waits for network I/O;
- Unstable: depends on external service availability, prone to flakiness;
- Side effects: may produce real data (e.g., creating users, charging).
The solution is to intercept and mock the network layer. Both pytest-httpx and httpx's built-in MockTransport can do this.
15.2 pytest-httpx Plugin: Match and Return
pytest-httpx is the most popular pytest plugin, providing request interception via fixtures:
pip install pytest-httpx httpx# tests/test_api_client.py
import pytest
import httpx
from myapp.client import GitHubClient
def test_get_user(consumer: httpx.Client) -> None:
"""Intercept requests via fixture, return preset response"""
# Register expected request and response
consumer.register_pattern(
httpx.MatchRequest("GET", "https://api.github.com/users/octocat"),
httpx.Response(200, json={"login": "octocat", "stars": 1000}),
)
client = GitHubClient()
user = client.get_user("octocat")
assert user["login"] == "octocat"
assert user["stars"] == 1000Match Granularity Control
MatchRequest supports precise matching of URL, method, headers, and body — ideal for tests that need to distinguish different request scenarios.
15.3 Shorthand Fixtures: client and async_client
pytest-httpx provides two convenient fixtures:
import pytest
import httpx
def test_simple_get(client: httpx.Client) -> None:
"""client fixture auto-registers patterns"""
client.register(
httpx.Response(200, json={"ok": True}),
match=[httpx.MatchRequest(url="https://httpbin.org/get")]
)
resp = client.get("https://httpbin.org/get")
assert resp.json() == {"ok": True}
@pytest.mark.asyncio
async def test_async_get(async_client: httpx.AsyncClient) -> None:
"""async_client fixture for async tests"""
async_client.register(
httpx.Response(200, json={"async": True}),
match=[httpx.MatchRequest(url="https://httpbin.org/get")]
)
resp = await async_client.get("https://httpbin.org/get")
assert resp.json() == {"async": True}15.4 Using MockTransport: Pure httpx Approach
Without relying on third-party plugins, httpx has built-in MockTransport:
import httpx
def mock_handler(request: httpx.Request) -> httpx.Response:
if "/users/" in str(request.url):
return httpx.Response(200, json={"id": 1, "name": "Alice"})
return httpx.Response(404)
def test_with_mock_transport() -> None:
with httpx.Client(transport=httpx.MockTransport(mock_handler)) as client:
resp = client.get("https://api.example.com/users/1")
assert resp.status_code == 200
assert resp.json()["name"] == "Alice"
resp = client.get("https://api.example.com/missing")
assert resp.status_code == 404MockTransport is suitable for simple scenarios that don't need pytest fixtures, or when using non-pytest test frameworks.
15.5 Testing Async Clients
Combine with pytest-asyncio to test async code:
pip install pytest-asyncio# Configure asyncio mode in conftest.py
import pytest
@pytest.fixture(scope="session")
def asyncio_mode():
return "auto" # auto-detect async def testsimport pytest
import httpx
import pytest_asyncio
@pytest.mark.asyncio
async def test_async_flow(async_client: httpx.AsyncClient) -> None:
async_client.register(
httpx.Response(200, json={"token": "abc123"}),
match=[httpx.MatchRequest(url="https://auth.example.com/token")]
)
async with async_client as client:
resp = await client.post(
"https://auth.example.com/token",
json={"username": "user", "password": "pass"}
)
assert resp.json()["token"] == "abc123"15.6 Complete Test Example: API Client
# myapp/client.py
import httpx
class APIClient:
def __init__(self, base_url: str, api_key: str):
self.base_url = base_url
self.api_key = api_key
def get_user(self, user_id: int) -> dict:
resp = httpx.get(
f"{self.base_url}/users/{user_id}",
headers={"Authorization": f"Bearer {self.api_key}"}
)
resp.raise_for_status()
return resp.json()
# tests/test_client.py
import pytest
import httpx
def test_get_user_success(client: httpx.Client) -> None:
client.register(
httpx.Response(200, json={"id": 42, "name": "Bob"}),
match=[
httpx.MatchRequest(
url="https://api.example.com/users/42",
headers={"Authorization": "Bearer test-key"}
)
]
)
from myapp.client import APIClient
api = APIClient("https://api.example.com", "test-key")
user = api.get_user(42)
assert user["name"] == "Bob"
assert user["id"] == 42
def test_get_user_not_found(client: httpx.Client) -> None:
client.register(
httpx.Response(404, json={"error": "not found"}),
match=[httpx.MatchRequest(url="https://api.example.com/users/999")]
)
from myapp.client import APIClient
api = APIClient("https://api.example.com", "test-key")
with pytest.raises(httpx.HTTPStatusError):
api.get_user(999)15.7 Chapter Summary
pytest-httpxplugin providesclient/async_clientfixtures andregister/register_patternmethods;MockTransportis httpx's built-in solution with no extra dependencies;- Combine with
pytest-asyncioto test async clients; - Tests should cover success, client errors (4xx), and server errors (5xx) paths.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Does the pytest-httpx client fixture automatically send real requests by default?
2. What is the handler function signature for httpx.MockTransport?
3. What is the correct decorator for testing async def functions in pytest-asyncio?
4. What is the main difference between register_pattern and register?
🛠️ Hands-on Practice
- Use pytest-httpx to test a simple REST client wrapping GET/POST, covering both success and 404 cases.
- Use MockTransport to implement an async HTTP test without depending on pytest-httpx.
- Write a test that verifies the client raises the correct exception with error message when the service returns 503.
After completing the exercises, move to Next chapter: Performance Optimization and Resource Management.