mcpField
P
pytest

pytest

github.com/imankulov测试与质量357 次安装2 周前更新
代码仓库

关于 pytest

pytest Standards 是一项规定 pytest 测试约定的技能,包含 Django 专用模式,帮助 Python 开发者统一组织、运行和验证测试。

主要功能

  1. 01按 Arrange-Act-Assert 组织测试
  2. 02安排测试文件和共享 fixture
  3. 03用参数化覆盖不同输入
  4. 04设置简洁的测试运行默认值
  5. 05提供 Django 数据库和 API 测试指导
  6. 06覆盖回归、缓存、模拟客户端及端到端测试

适用场景

  1. 01为 Django API 端点编写测试
  2. 02运行测试套件并排除端到端测试
  3. 03为集成问题添加回归测试
  4. 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。

评论

支持 Markdown