第 15 章 · pytest 集成测试
本章目标:掌握 pytest-httpx 插件和 pytest-asyncio,写出可复现、可并行的 HTTP 客户端测试。
15.1 为什么 HTTP 测试需要专门工具
直接发真实 HTTP 请求做测试有三个问题:
- 速度慢:每次测试都要等待网络 I/O;
- 不稳定:依赖外部服务的可用性,容易 flaky;
- 有副作用:可能产生真实数据(如创建用户、扣款)。
解决方案是拦截并 mock 网络层。pytest-httpx 和 httpx 自带的 MockTransport 都能做到这一点。
15.2 pytest-httpx 插件:匹配即返回
pytest-httpx 是最流行的 pytest 插件,通过 fixture 提供请求拦截:
bash
pip install pytest-httpx httpxpython
# tests/test_api_client.py
import pytest
import httpx
from myapp.client import GitHubClient
def test_get_user(consumer: httpx.Client) -> None:
"""用 fixture 拦截请求,返回预设响应"""
# 注册期望的请求和响应
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 粒度控制
MatchRequest 支持精确匹配 URL、方法、headers、body,适合需要区分不同请求场景的测试。
15.3 简写 fixture:client 与 async_client
pytest-httpx 提供两个便捷 fixture:
python
import pytest
import httpx
def test_simple_get(client: httpx.Client) -> None:
"""client fixture 自动注册 pattern"""
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 用于异步测试"""
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 使用 MockTransport:纯 httpx 方案
不依赖第三方插件时,httpx 内置 MockTransport:
python
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 适合不需要 pytest fixture 的简单场景,或在非 pytest 测试框架中使用。
15.5 测试异步客户端
结合 pytest-asyncio 测试异步代码:
bash
pip install pytest-asynciopython
# conftest.py 中配置 asyncio 模式
import pytest
@pytest.fixture(scope="session")
def asyncio_mode():
return "auto" # 自动识别 async def 测试python
import 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 完整测试示例:API 客户端
python
# 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 本章小结
pytest-httpx插件提供client/async_clientfixture 和register/register_pattern方法;MockTransport是 httpx 内置方案,无需额外依赖;- 结合
pytest-asyncio可测试异步客户端; - 测试应覆盖成功、客户端错误(4xx)、服务端错误(5xx)三种路径。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. pytest-httpx 的 client fixture 默认是否自动发送真实请求?
2. httpx.MockTransport 的处理器函数签名是什么?
3. pytest-asyncio 中测试 async def 函数的正确装饰器是?
4. register_pattern 与 register 的主要区别是?
🛠️ 动手实践
- 用 pytest-httpx 测试一个封装了 GET/POST 的简单 REST 客户端,覆盖成功和 404 两种情况。
- 用 MockTransport 实现一个不依赖 pytest-httpx 的异步 HTTP 测试。
- 编写一个测试,验证客户端在 503 服务不可用时抛出正确异常并包含错误信息。
完成练习后,进入下一章:性能优化与资源管理。