跳转至

绿色的 “0 tests” 与真正的测试入口

你运行 pytest,终端没有红色。仔细看才发现:

collected 0 items

原因是文件名叫 plan_tests.py,没有匹配 pytest 默认的 test_*.py*_test.py。没有失败,不代表有测试被执行。测试基线必须先回答“收集了什么”, 再回答“如何选择”和“结果是否稳定”。

运行环境

本章基于 pytest 9.1.1 的已验证行为和 stable 官方文档。命令默认从仓库根目录 执行;项目使用 pyproject.toml 保存 pytest 配置。

1. 先确认 pytest 看见了哪些测试

概念|pytest 测试发现 pytest 在执行前根据配置、文件名、类名和函数名规则建立待运行集合。命令退出为 绿色而集合为空,只能说明没有测试失败,不能说明目标测试被发现。

一个清楚的文件树可以是:

pyproject.toml
src/learning_assistant/
tests/
├── conftest.py
├── unit/
│   └── test_plans.py
└── integration/
    └── test_plans_api.py

pytest 无参数运行时,从配置的 testpaths 或当前目录开始递归。默认查找 test_*.py*_test.py,再收集以 test 开头的函数,以及 Test 开头且无 自定义 __init__ 的类中的测试方法。

先用收集模式核对:

python -m pytest --collect-only -q

概念|pytest node ID pytest 为每个收集项生成的定位表达,通常由文件路径和测试名称用 :: 连接;它 可以用来核对集合,也可以精确运行单个案例。

你应该看到预期 node ID,例如:

tests/unit/test_plans.py::test_build_plan_rejects_existing_topic
tests/integration/test_plans_api.py::test_create_plan_reports_topic_conflict

python -m pytestpytest 大体等价,但前者按 Python 模块执行语义把当前目录 加入 sys.path。项目应选择一个从仓库根稳定工作的入口并写入 README,而不是 依赖 IDE 临时调整 import path。

2. 用属性和定位表达选择集合

概念|pytest marker 为测试声明可选择属性,并通过 -m 组成有意义的集合。自定义 marker 应在配置中 注册;启用 strict markers 后,未知或拼错的名字会直接失败。

三类入口可以这样设计:

# 快速 core 单元集合
python -m pytest tests/unit

# 真实 FastAPI 应用集成集合
python -m pytest -m integration

# 完整集合(默认不遗漏任何核心层)
python -m pytest

在配置中注册 marker:

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["--strict-markers"]
markers = [
  "integration: exercises the in-process FastAPI application boundary",
]

然后在集成测试上标记:

import pytest

pytestmark = pytest.mark.integration

marker 表达测试属性,目录表达结构位置,node ID 用于精确定位单个案例:

python -m pytest \
  tests/integration/test_plans_api.py::test_create_plan_reports_topic_conflict

未注册的自定义 marker 会产生 warning;启用 strict markers 后,拼错名字会直接 报错。这比静默选错集合更安全。不要用模糊的 -k integ 代替明确集合:它按名称 表达式匹配,可能误选或漏选。

3. 一次通过只是起点

为了检查 P04 的隔离假设,可以执行:

python -m pytest
python -m pytest
python -m pytest tests/unit
python -m pytest tests/integration

也可以按不同 node ID 顺序运行两个敏感案例。相同代码、配置和受控前置条件下, 这些运行方式应给出一致语义结果。若完整集合第二次才失败,优先检查:

  • 模块或 session 共享可变状态;
  • 未清理的环境变量、文件或 dependency override;
  • 读取真实当前时间或随机值;
  • 测试依赖前一个案例创建数据。

重复运行是复现手段,不是修复。自动 retry、无依据 sleep 或放宽断言会隐藏测试 信号。应构造最小失败顺序,找到未受控变量,再恢复原有 oracle。

4. 红色结果也要能告诉你下一步

一个失败大致可能来自四处:

分类 典型证据 下一步
实现缺陷 实际值稳定偏离契约 缩小到最小行为并修实现
测试缺陷 预期与权威契约不一致 修 oracle,但保留测试目标
数据污染 换序/单跑结果不同 查 fixture、共享状态和清理
环境问题 import/依赖/配置缺失 核对可追溯依赖与工作目录

pytest 的断言差异、traceback、fixture 名称和 node ID 是诊断输入。看到 expected 201, got 409 且单跑通过,优先怀疑已有 topic 状态;看到所有测试在 collection 阶段 ModuleNotFoundError,则尚未运行产品行为,先检查安装与 import 入口。

跳过、xfail 或筛选不能无说明地移除核心失败。它们有合法用途,但“先让面板变绿” 不是用途。

常见误区

  • 0 tests 也是通过。 它只说明没有测试失败,不能证明预期集合被发现。
  • integration 就是 slow。 本课 marker 表达真实 FastAPI 边界,不以耗时命名。
  • 默认命令跑快速集合。 若 README 称它为完整集合,就会静默漏掉核心集成。
  • 重跑后绿了就关闭问题。 偶发变化本身就是未控制状态的证据。

本章小结

稳定入口包含三层保证:discovery 能说明收集成员;目录、注册 marker 和 node ID 能选择有意义集合;单独、连续和换序运行得到一致语义结果。失败输出还应帮助区分 实现、测试、数据与环境问题,而不是只给一个红灯。

练习

审查以下配置和命令:

[tool.pytest.ini_options]
testpaths = ["tests"]
@pytest.mark.integartion
def test_api(client): ...
pytest -m integration

为什么命令可能安静地漏掉该测试?怎样修正并让拼写错误尽早失败?

练习解析

marker 拼成了 integartion,配置也没有注册任何 marker。运行 -m integration 不会选中拼错的测试,pytest 只会对未知 marker 给出 warning;如果忽略 warning, 集合会安静地缺失。

应在 pyproject.toml 注册 integration,启用 --strict-markers,修正装饰器拼写; 先用 --collect-onlypytest --markers 核对,再运行单元、集成和默认完整集合, 确认默认入口没有排除集成测试。

参考资料