关于 pytest
pytest Standards 是一项规定 pytest 测试约定的技能,包含 Django 专用模式,帮助 Python 开发者统一组织、运行和验证测试。
主要功能
- 01按 Arrange-Act-Assert 组织测试
- 02安排测试文件和共享 fixture
- 03用参数化覆盖不同输入
- 04设置简洁的测试运行默认值
- 05提供 Django 数据库和 API 测试指导
- 06覆盖回归、缓存、模拟客户端及端到端测试
适用场景
- 01为 Django API 端点编写测试
- 02运行测试套件并排除端到端测试
- 03为集成问题添加回归测试
- 04验证不同请求使用独立缓存
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ imankulov/ skills --skill pytest安装后在 Claude Code 中输入/pytest确认已加载。
详细介绍
Pytest 标准
针对 pytest 的主观测试约定,包含 Django 专用模式。
AAA 模式(准备-执行-断言)
每个测试都用 # Arrange、# Act、# Assert 注释分隔准备、执行和验证。准备阶段主要通过夹具完成。不需要的部分应直接省略,不要留空。逻辑相关时可以使用多个断言。
def test_user_service_create(sample_user, db_session):
# Arrange
service = UserService(db_session)
# Act
created_user = service.create(sample_user)
# Assert
assert created_user.id is not None
assert created_user.name == sample_user.name
测试组织
- 将测试存放在每个模块内的
tests/子目录中。 - 测试文件命名应与被测模块对应:
services.py->tests/test_services.py。 - 编写扁平的 pytest 函数,不要使用测试类。
夹具
创建夹具前,按以下顺序查找现有夹具:根目录的 conftest.py、模块的 tests/conftest.py,然后是当前测试文件。只有没有合适的夹具时才创建新的。
模块的多个测试文件共享的夹具应放在 tests/conftest.py 中,而不是单独的测试文件中。按作用域排序夹具:session -> module -> function。
参数化
使用 @pytest.mark.parametrize 测试多组输入/输出组合,而不是重复编写几乎相同的测试主体。
运行测试
# All tests (excluding e2e)
uv run pytest -m "not e2e" -q --tb=short
# Stop at the first failure, then iterate on only what failed
uv run pytest -m "not e2e" -q --tb=short -x
uv run pytest --lf -q --tb=short
# Full assertion diff for one test, once you know which one failed
uv run pytest path/to/test_file.py::test_function_name -vv
使用 -q --tb=short 运行。-q 会将通过的测试折叠为进度点,同时保留 FAILED path::test_name - reason 摘要,因此随着测试套件增大,输出仍然简洁;-v/-vv 会为每个通过的测试打印一行,在一个包含 500 个测试且全部通过的测试套件中,约占用 10k tokens 的上下文,而 -q 仅约占用 150。
不要使用 -s。Pytest 已会重放失败测试的 Captured stdout call、Captured stderr call 和 Captured log call 部分;-s 只会额外显示通过测试的输出。
只对单个节点 ID 使用 -vv,不要对整个测试套件使用。默认详细程度下,pytest 会将较长的断言差异裁剪为不同的项目,并打印 use -vv to show——如果裁剪后的差异不够清楚,就重新运行该单个测试。
默认排除端到端测试——它们会调用外部服务,且运行缓慢、容易不稳定。只有当更改直接影响端到端测试覆盖的代码时,才运行端到端测试。
在 pyproject.toml 中固定这些默认设置,让每次运行都自动采用它们:
[tool.pytest.ini_options]
addopts = "-q --tb=short"
Django 测试数据库
对于 Django 项目,在 addopts 中添加 --reuse-db,让测试数据库在运行之间保留,而不是每次都删除并从迁移重新构建:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings"
addopts = "-q --tb=short --reuse-db"
添加迁移无需额外标志——--reuse-db 只会跳过删除和重建数据库,不会跳过随后执行的 migrate,因此新迁移会在下次运行时应用。与 --no-migrations 搭配会导致问题:此时架构由 run_syncdb 构建,它会创建缺失的表,但不会修改现有表,因此此后每次架构变更都需要使用 --create-db。
当复用的数据库确实损坏时,使用 --create-db;这通常表现为与当前模型相矛盾的架构错误:
OperationalError: table myapp_thing has no column named shade— 现有迁移曾被直接修改,因此数据库已将其记录为已应用,不会再次运行IntegrityError: NOT NULL constraint failed: ...或InconsistentMigrationHistory— 数据库仍保留当前分支中已不存在的迁移
uv run pytest --create-db
--create-db 会在本次运行中覆盖 --reuse-db,并将重建后的数据库保留到下次运行,因此只需传入一次,不要将它添加到 addopts。
Django 数据库访问
如果测试的任一夹具(直接或间接)依赖 db,该测试就会自动获得数据库访问权限。只有没有任何夹具引入此依赖时,测试才需要 @pytest.mark.django_db。
# No decorator — client_with_user brings db via the user fixture
def test_create_report_success(client_with_user):
response = client_with_user.post("/api/v1/reports", ...)
assert response.status_code == 200
# Decorator needed — client alone doesn't bring db
@pytest.mark.django_db
def test_create_report_requires_auth(client):
response = client.post("/api/v1/reports", ...)
assert response.status_code == 401
Django API 测试
使用 Django 测试客户端测试 API 端点:未认证请求使用 client 夹具,已认证请求使用 client_with_user。不要使用 ninja.testing.TestClient。发送 JSON 时设置 content_type="application/json"。对于会修改数据库的测试,添加 ORM 断言来验证最终状态:
assert User.objects.get(id=user.id).settings.theme == "dark"
Mock 客户端测试
测试环境通过 .env.test 使用真实的模拟提供方来配置客户端,因此请直接使用模拟客户端,并断言其捕获的数据。直接引用模拟客户端中的类常量,并保持全局状态不变(不要调用 make_empty() 或类似方法)。不要模拟 get_*_client() 工厂函数——模拟它们会破坏依赖链。
def test_event_is_captured(posthog_client):
some_function_that_captures_event()
assert len(posthog_client.events) == 1
assert posthog_client.events[0]["event"] == "oauth_completed"
HTTP 响应断言
对于较短的响应,断言完整响应正文(assert response.json() == {...})。对于较长的响应,断言关键字段,而不是整个有效负载。
回归测试
在最接近的稳定公共边界复现已报告的错误。回归测试应使用导致失败的请求格式、输入规范化方式和输出,而不应只测试新添加的辅助函数,或断言某个参数已传递。
对于集成,若测试目标是生成的提供方请求,应避免发起真实的第三方调用。在配置的模拟客户端边界捕获请求,然后断言导致错误的确切筛选条件、规范化标识符或缓存输入。对于复杂的辅助函数,可以保留一个专注的单元测试,但不要用它替代边界回归测试。
缓存标识测试
行为被缓存时,请列出所有可能改变结果的输入。每项输入都必须出现在缓存键中,或解析为一个包含在缓存键中的标识,例如所选属性 ID。添加测试,证明语义不同的请求不会共用缓存条目,尤其是租户、区域设置、范围、筛选条件、权限和日期输入。
如果更强的已解析标识已能区分结果,就不要添加冗余的键组成部分。在测试中说明该标识,避免后续修改解析逻辑时这一假设被悄然破坏。
E2E 测试
如果测试符合以下任一条件,应应用 @pytest.mark.e2e 标记,使本地 pytest 运行默认跳过该测试:
- 调用外部/第三方服务(真实的大模型提供方、Stripe、GitHub API 等)
- 需要付费资源或消耗 API 配额
- 需要标准开发环境之外的额外设置或凭据(在线数据库、浏览器驱动程序、密钥、访问内部主机的网络)
- 驱动真实浏览器(Playwright、Selenium)
- 明显耗时(数秒)或本身容易出现不稳定
如果测试启动进程内服务器、模拟目标或本地子进程,并以自包含方式运行,那么它属于集成测试,而非 E2E 测试——即使它会在随机端口启动真实的 uvicorn 并发送 localhost 请求,也不例外。
只需添加标记即可;无需检查 os.environ。CI 会通过 pytest -m "not e2e" 跳过这些测试。若要标记文件中的所有测试,请在模块级别设置 pytestmark = pytest.mark.e2e。