介绍
pytest 是一个功能强大、灵活且易用的 Python 测试框架,用于编写和运行单元测试、集成测试等。它是 Python 社区中最受欢迎的测试工具之一,相比内置的 unittest 模块,pytest 提供了更简洁的语法、更丰富的功能和更好的扩展性。
- 官网: pytest.org
- 安装: 通过 pip 安装 pip install pytest。
- 版本要求: 当前使用的是 pytest-8.3.5(根据你的输出),这是一个较新的版本,支持 Python 3.12。
不需要继承特定的基类(如 unittest.TestCase),只需写普通的 Python 函数。使用 assert 语句即可进行断言,无需复杂的 self.assertEqual 等方法。
- 不需要继承特定的基类(如 unittest.TestCase),只需写普通的 Python 函数。
- 使用 assert 语句即可进行断言,无需复杂的 self.assertEqual 等方法。
配置
pytest.ini
[pytest]
testpaths = tests
pythonpath = .
# pytest -v
在tests/test_exampel:
import pytest
def test_example():
assert 1 == 1
这就是最简单的例子了
运行:
pytest -v
常用的写法
🧩 一、断言能力(assert)
你可以断的不只是 ==,而是任何 Python 表达式:
def test_math():
assert 1 + 1 == 2
assert "hello".upper() == "HELLO"
assert isinstance(3.14, float)
assert 5 in [1, 2, 3, 4, 5]
🧪 二、参数化测试(parametrize)
import pytest
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(2, 2, 4),
(3, 5, 8)
])
def test_add(a, b, expected):
assert a + b == expected
🔧 三、使用 fixture 管理重复逻辑(数据准备/清理)
user_data() 只写一遍,多个测试用,自动注入。
@pytest.fixture
def user_data():
return {"name": "Alice", "age": 30}
def test_user_name(user_data):
assert user_data["name"] == "Alice"
def test_user_age(user_data):
assert user_data["age"] == 30
🧰 四、捕捉异常
def divide(a, b):
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
🕹️ 五、分组测试(class 测试组织)
class TestMath:
def test_add(self):
assert 1 + 1 == 2
def test_mul(self):
assert 2 * 3 == 6
你可以运行特定类的测试:
pytest tests/test_example.py::TestMath
运行单个方法:
pytest tests/test_example.py::TestMath::test_add
🧼 六、运行顺序和 setup/teardown
@pytest.fixture
def setup_and_teardown():
print("\nsetup")
yield
print("teardown")
def test_abc(setup_and_teardown):
print("running test")
assert True
🎁 七、使用命令行参数和调试技巧
• -v:显示详细信息
• -x:遇到失败就停止
• -k "关键字":只运行包含关键字的测试
• --maxfail=2:最多失败两个就停
✅ 总结格式
pytest <文件路径>::<类名>::<方法名>
测试FastAPI 路由
配合 FastAPI 提供的 TestClient(同步测试)或 AsyncClient(异步测试),你可以轻松写出像这样测试 API 的代码:
同步测试
@app.get("/ping")
def ping():
return {"msg": "pong"}
测试代码:
from fastapi.testclient import TestClient
from routers.sse_router import app
client = TestClient(app)
def test_ping():
response = client.get("/ping")
assert response.status_code == 200
assert response.json() == {"msg": "pong"}
异步测试
使用异步插件:
pip install pytest-asyncio
在pytest.ini添加:
# pytest.ini
[pytest]
asyncio_mode = auto
还需要:
pip install "httpx[http2]==0.27.0"
🔍 为什么需要 [http2]?
AsyncClient(app=...) 这个功能依赖了 h11, h2, anyio, httpcore, asgiref 等 ASGI 支持组件。
httpx 的 extra 安装项 [http2] 会自动把它们都装上。
问题 | 原因 | 解决方式 |
---|---|---|
502 Bad Gateway | httpx.app=app 方式不再可靠 | ✅ 改用 transport=ASGITransport(app=…) |
DeprecationWarning | app= 参数已弃用 | ✅ 同上 |
路由未注册/未启动 | startup 未被触发 | ✅ 使用 ASGITransport 能模拟完整生命周期 |
测试代码:
@app.get("/hello")
async def hello():
return {"message": "Hello async!"}
测试:
@pytest.mark.asyncio
async def test_hello():
# ✅ 推荐使用 transport 显式传入 app,避免 DeprecationWarning & 502 错误
# 告诉 httpx:“别走网络了,直接通过 ASGI 协议,调用 app 的处理逻辑。”
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/hello")
assert response.status_code == 200
assert response.json() == {"message": "Hello async!"}
ASGITransport 是 httpx 专门用于测试 FastAPI(ASGI app)的一种机制,模拟 HTTP 请求过程而不需要真正启动一个 HTTP 服务器。
🔹 1. FastAPI 是 ASGI 应用
FastAPI 完全基于 ASGI(Asynchronous Server Gateway Interface) 构建,不是 WSGI。
你平时运行 FastAPI 都是通过 uvicorn(ASGI server) 启动的。
🔹 2. 测试时你并不想真的启动一个服务器!
我们写测试时,不希望真的启动 uvicorn 开一个端口来跑接口。
而是希望:
• 直接在内存中发请求给 app
• 不用开端口、开线程、搞网络
这就需要一个“模拟器”,来让 AsyncClient 通过 app 发请求,而不是通过 http。
✅ 所以为什么要用 ASGITransport?
原因 | 说明 |
---|---|
✅ 避免真的启动 HTTP 服务(更快) | 测试运行速度更快、无需网络 |
✅ 支持 FastAPI 全生命周期(startup/shutdown) | 避免测试中出现未注册路由、连接池等问题 |
✅ 替代弃用的 AsyncClient(app=app) | 官方推荐方式 |
✅ 支持异步测试 (async def) | 与 pytest-asyncio 完美结合 |
🔍 异步 vs WSGI vs ASGI 的区别总结
名称 | 类型 | 说明 |
---|---|---|
WSGI | 同步 | Django/Flask(旧接口标准) |
ASGI | 异步 | FastAPI/Starlette(新接口标准) |
Uvicorn | ASGI Server | 启动 FastAPI 的服务器 |
ASGITransport | 模拟传输层 | 专门用于测试 ASGI 应用,无需真正启动服务 |
用法 | 示例 |
---|---|
测试 POST 请求 | client.post(“/login”, json={“user”: “xx”}) |
添加 Header / token | client.get(“/me”, headers={“Authorization”: “Bearer xxx”}) |
发送 query 参数 | client.get(“/items”, params={“page”: 1}) |
上传文件 | client.post(“/upload”, files={“file”: (“name.txt”, b”content”)}) |
使用 fixture 提供 token | 配合 @pytest.fixture 注入 headers |
能力 | 支持情况 |
---|---|
GET/POST/PUT/DELETE 请求测试 | ✅ 有 |
登录 token 验证 | ✅ 有 |
路由参数校验 | ✅ 有 |
异步接口测试 | ✅ 有(用 httpx.AsyncClient) |
接口前后 setup 清理 | ✅ 支持 fixture |