pytest:Python 测试框架的事实标准,从 assert 反射到 fixture 体系
posts posts 2026-06-14T21:13:12+08:00pytest 是 14.4k stars 的 Python 测试框架事实标准。从 assert 反射讲起,系统拆解 fixture、parametrize、conftest、插件生态等机制与反模式。技术笔记Python, 测试框架pytest:Python 测试框架的事实标准,从 assert 反射到 fixture 体系
pytest 的入门门槛很低,一个
assert就能写测试。但真正让它难以替代的是 fixture 体系、parametrize、conftest.py 加上 1300 多个插件——从一行 demo 到十万级用例的测试工程都能撑住。新手常把它当 unittest 替代品,老手把它当测试编排框架,两种用法并不冲突。
这篇文档按"先会跑、再拆机制、后避坑"的顺序展开。 读完你会得到三样东西:一是能独立装好 pytest 并把第一个用例跑通(含常见 CLI 参数的用途);二是能说清 fixture、parametrize、conftest.py、marker 四套机制各自解决什么问题、彼此怎么配合,而不是只背 API;三是能看懂一个完整项目的测试是怎么被组织起来的,并知道测试变慢、用例互相污染时该往哪儿查。前四章适合初次上手的人从头读,第五章开始进入机制,已有基础的人可以直接跳到对应章节。
一、项目坐标
| 字段 | 值 |
|---|---|
| 仓库 | pytest-dev/pytest |
| 主语言 | Python(支持 Python 3.10+ 或 PyPy3) |
| Stars | 14.4k |
| Forks | 3.3k |
| License | MIT |
| 创始人 | Holger Krekel(2004 年至今) |
| 插件数 | 1300+ 外部插件(官方收录列表见 plugin_list) |
数据核验于 2026-08(GitHub API 与 PyPI);当前最新版为 9.1.x(9.1.1 发布于 2026-06-19)。
pytest 的 README 开篇就是定位:“makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries”。“easy → scales” 这条承诺贯穿后续所有设计。
二、为什么 pytest 成了事实标准
Python 生态里测试框架不止一个:unittest(标准库)、nose2、doctest 都能写测试。pytest 在十几年里几乎一统江湖,原因有三。
1. 反射式 assert 失败信息。 这是新手最先感知到的不同。unittest 要求你写 self.assertEqual(a, b),pytest 允许直接写 assert a == b。断言失败时,pytest 通过 AST(抽象语法树,Abstract Syntax Tree)改写拿到 a、b 的实际值,再求值对比,打印出"哪个变量、什么值、为什么不等"。
官方 README 给的最小例子:
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5运行 pytest 的输出不是干巴巴的 AssertionError,而是:
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)where 4 = inc(3) 这一行是关键:pytest 把 inc(3) 重新求值一遍,告诉你"4 是从哪来的"。这种内省(introspection)在处理嵌套表达式、长链调用、复杂数据结构比较时尤其救命。
2. 自动发现。 不需要写 suite、不需要继承 TestCase、不需要注册。pytest 默认扫描当前目录及子目录下文件名匹配 test_*.py 或 *_test.py 的文件,把其中以 test_ 开头的函数和 Test 开头的类里的 test_ 方法自动收集为测试用例。这套约定让"加一个测试 = 加一个函数"成为可能。
3. 插件架构。 pytest 把几乎所有非核心能力都做成插件——pytest-cov(覆盖率)、pytest-mock(mock 包装)、pytest-django(Django 集成)、pytest-asyncio(异步测试)、pytest-xdist(并行执行)——这 1300+ 插件构成了扩展生态,单元测试到端到端测试都有对应的工具。
合起来看,unittest 在新项目里基本退到"兼容性选项"的位置。
三、五分钟快速上手
3.1 安装
前置条件:Python 3.10+(或 PyPy3)。建议在虚拟环境里装,避免污染系统解释器。
pip install pytest验证安装:
pytest --version
# pytest 9.1.13.2 第一个测试
# test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 4 # 故意写对,演示通过$ pytest
============================= test session starts ==============================
collected 1 item
test_sample.py . [100%]
============================== 1 passed in 0.01s ===============================把 == 4 改成 == 5,再跑一次,你会看到前面那段 where 4 = inc(3) 的反射输出。
3.3 常用命令行参数
| 参数 | 作用 |
|---|---|
pytest | 递归扫描当前目录 |
pytest test_x.py | 只跑指定文件 |
pytest -k "login" | 按名字关键字过滤 |
pytest -m slow | 按 marker 过滤(需要先注册 marker) |
pytest -x | 遇到第一个失败就停 |
pytest --maxfail=3 | 最多累计 N 个失败后停 |
pytest -v | 详细模式(显示每个测试名) |
pytest -s | 不捕获 print(--capture=no) |
pytest --tb=short | 控制 traceback 详细度(long/short/line/no) |
pytest -p no:cacheprovider | 禁用某个插件 |
pytest --lf | 只重跑上次失败的用例(--last-failed) |
pytest --durations=10 | 列出耗时最长的 10 个用例 |
日常用到的 CLI 参数基本就是这 12 个,剩下的能力都通过 fixture 和插件暴露。
四、pytest 机制总览
在深入 fixture 之前,先看一眼 pytest 的机制全貌。表里每套机制都标了它解决什么问题、作用范围、跟谁配合。
| 机制 | 解决什么问题 | 作用范围 | 与谁配合 |
|---|---|---|---|
| fixture | 测试资源准备与清理(setup/teardown) | 通过 scope 控制,从 function 到 session | parametrize、conftest.py |
| parametrize | 把一组数据展开成独立测试用例 | 测试函数级(通过 @pytest.mark.parametrize) | fixture(参数名引用 fixture) |
| conftest.py | 跨文件共享 fixture 和 hook,无需 import | 目录级(对该目录及子目录所有测试可见) | fixture、hook |
| marker | 给测试打标签,按标签筛选执行 | 测试函数/类级(通过 @pytest.mark.xxx) | CLI -m 参数 |
| hook | 在 pytest 执行流程的特定节点插入自定义逻辑 | 会话级(通过 conftest.py 或插件注册) | conftest.py、插件 |
| 插件 | 扩展 pytest 核心能力(覆盖率、mock、并行等) | 会话级(通过 pip install + entry_points 注册) | hook、fixture |
遇到"这个 fixture 在哪些文件里可见"、“marker 和 parametrize 的区别是什么"这类问题时,回来看这张表比翻 API 文档快。
五、Fixture 体系
assert 和自动发现让你愿意用 pytest;fixture 决定了你能不能把它用深。
5.1 fixture 是什么
fixture 是一个由 @pytest.fixture 装饰的函数,职责是"准备测试需要的资源,并在测试结束后清理”。测试函数通过同名参数声明自己需要哪个 fixture。pytest 负责在调用测试前执行 fixture,结束后按依赖逆序清理。
import pytest
@pytest.fixture
def db():
print("连接数据库")
yield {"conn": "fake-conn"}
print("关闭连接") # yield 之后的代码就是 teardown
def test_query(db):
assert db["conn"] == "fake-conn"yield 是 fixture 的分界点:之前是 setup,之后是 teardown。一个函数同时承载 setup 和 teardown,比 unittest 的 setUp/tearDown + setUpClass/tearDownClass 两对方法要清晰。
5.2 scope:fixture 的生命周期
fixture 默认 scope="function",意味着每个测试函数都拿一份全新的实例。pytest 提供 5 个 scope:
| scope | 含义 | 典型场景 |
|---|---|---|
function(默认) | 每个测试函数一份 | 临时对象、轻量 mock |
class | 每个测试类一份 | 类内共享状态 |
module | 每个 .py 文件一份 | 整个文件共用一个 client |
package | 每个包(带 __init__.py)一份 | 跨文件共享但要包级隔离 |
session | 整个测试会话一份 | 数据库连接池、HTTP client |
scope 越大,setup 次数越少,测试越快,但要警惕"测试间状态污染"。session 级 fixture 几乎一定要配合"只读 + 内部复制"使用,否则后续测试会被前面的副作用拖死。
from sqlalchemy import create_engine # 此处以 SQLAlchemy 为例
@pytest.fixture(scope="session")
def engine():
# 全测试会话只启动一次重型资源
eng = create_engine("sqlite:///:memory:")
yield eng
eng.dispose()5.3 autouse:隐式 fixture
如果某个 fixture 必须对所有测试都生效,可以加 autouse=True,pytest 会自动调用它,测试函数不用写参数。
@pytest.fixture(autouse=True)
def reset_config():
Config.load_defaults()
yield
Config.clean()autouse 是把双刃剑:好处是少写一行参数,代价是新人读测试时不知道背后跑了什么。团队代码风格上,autouse 只建议用在"环境级别的副作用清理"(如重置全局配置、清空 mock 缓存),业务准备逻辑用显式参数。
5.4 factory fixture:参数化资源
有时候测试需要"一组"资源,而不是"一个"资源。factory fixture 的模式:fixture 返回一个工厂函数,测试调用这个函数创建新实例。
@pytest.fixture
def make_user(db):
created = []
def _make(name, role="user"):
u = db.create_user(name=name, role=role)
created.append(u)
return u
yield _make
# 清理:删除所有创建的 user
for u in created:
db.delete_user(u)
def test_admin(make_user):
admin = make_user("alice", role="admin")
assert admin.can_publish()factory 模式把"fixture 管理资源生命周期"和"测试控制资源内容"解耦——fixture 只管怎么建和怎么删,测试只管建什么和怎么用。
5.5 fixture 之间的依赖
fixture 可以依赖其他 fixture——参数列表写名字就行。
@pytest.fixture
def db():
return Database(":memory:")
@pytest.fixture
def user_repo(db):
return UserRepository(db)
def test_create(user_repo):
user_repo.create("bob")
assert user_repo.count() == 1pytest 会先算依赖图,按拓扑序逐层 setup。这样可以把"重型资源 → 业务封装 → 业务对象"逐层封装,测试只用关心"我需要哪个业务对象",不用关心它依赖什么。
六、parametrize——数据驱动测试
fixture 解决"怎么准备环境",parametrize 解决"怎么准备数据"。
import pytest
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert a + b == expected跑一次,pytest 会展开成 4 个独立测试用例,每个用例会显示自己的 nodeid(如 test_add[1-2-3]),失败时精确定位到具体数据行。
给每条数据起个可读的名字,CI 日志会更友好:
@pytest.mark.parametrize("a,b,expected", [
pytest.param(1, 2, 3, id="positive"),
pytest.param(0, 0, 0, id="zero"),
pytest.param(-1, 1, 0, id="mixed-sign"),
])
def test_add(a, b, expected):
assert a + b == expected输出会变成 test_add[positive] test_add[zero],一眼就能看到是哪类用例挂掉。
parametrize 还能和 fixture 叠加:把 fixture 名字写进参数列表,pytest 会先把 fixture 跑起来,再用 parametrize 展开数据。fixture × parametrize 的笛卡尔积是数据驱动测试的常用组合。
七、conftest.py——跨文件共享
写测试多了,很多 fixture 在多个文件里都要用。把 fixture 写进 conftest.py,pytest 会自动让它对该目录及子目录下的所有测试可见,不需要 import。
project/
├── conftest.py # 公共 fixture,对整个 project 可见
├── tests/
│ ├── conftest.py # tests 层 fixture,对 tests/ 下可见
│ ├── unit/
│ │ ├── conftest.py
│ │ └── test_foo.py
│ └── integration/
│ ├── conftest.py
│ └── test_bar.pyconftest.py 的可见性规则:fixture 树和目录树同构,靠近根的 conftest 越"通用",靠近叶子的 conftest 越"专用"。这是大型项目里 fixture 能铺开的原因。
不要在 conftest.py 里写测试函数——pytest 不会收集它们,文件名虽然带 test 也无效。conftest.py 只放 fixture、hook、plugin 配置。
八、内置常用 fixture
pytest 自带了一批"开箱即用"的 fixture,覆盖测试 90% 的副作用处理需求:
| fixture | 作用 |
|---|---|
tmp_path | 提供一个唯一的临时目录(pathlib.Path 类型),测试结束自动清理 |
tmp_path_factory | session 级临时目录工厂 |
monkeypatch | 临时修改属性/环境变量/字典项,测试结束自动还原 |
capsys / capfd | 捕获 print / 文件输出 |
caplog | 捕获 logging 输出 |
pytester | 给插件作者用,跑"会产出测试的测试" |
request | 提供当前测试的元信息(函数名、参数、marker) |
举例——monkeypatch 是写单元测试时改环境变量最干净的方式:
import mypkg.config # 假设 mypkg 已安装,config 模块有 DEBUG 属性
def test_debug_mode(monkeypatch):
monkeypatch.setenv("DEBUG", "1")
monkeypatch.setattr("mypkg.config.DEBUG", True)
assert mypkg.config.DEBUG is True
# 测试结束,DEBUG 自动还原tmp_path 也是必备:
def test_write_report(tmp_path):
target = tmp_path / "report.txt"
data = "## 标题\n正文内容"
write_report(target, data) # 假设 write_report 已定义
assert target.read_text().startswith("##")capsys / caplog 让"测 print 输出"和"测 log 输出"成为可能,避免在测试里塞一堆 print 调试。
九、Marker 系统——给测试打标签
marker 用来给测试"打标签"——跳过、加预期失败、按标签分组执行。
import pytest
@pytest.mark.skip(reason="还没修")
def test_broken():
assert False
@pytest.mark.xfail(reason="已知 bug")
def test_known_bug():
assert 1 + 1 == 3
@pytest.mark.slow
def test_heavy():
import time; time.sleep(10)命令行按 marker 过滤:
pytest -m slow # 只跑慢测试
pytest -m "not slow" # 跑所有非慢测试自定义 marker 必须在 pytest.ini / pyproject.toml 里注册,否则 pytest 8+ 会发出 PytestUnknownMarkWarning:
[tool.pytest.ini_options]
markers = [
"slow: 标记耗时较长的集成测试",
"integration: 跨服务集成测试",
]marker 的命名应该表达"为什么跳过/为什么分组",而不是"测试在做什么"。前者是"业务维度"(slow / integration / smoke),后者是"测试名"(test_login)——后者已经能通过测试名表达,再加 marker 就是冗余。
十、运行 unittest
pytest 兼容 unittest——所有继承 unittest.TestCase 的类 pytest 都能跑。
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual("foo".upper(), "FOO")pytest 直接执行。unittest 的 setUp/tearDown、@unittest.skip 装饰器全部生效,断言失败时同样享受 pytest 的反射式输出。
这条兼容性的实际意义:老项目可以渐进式迁移——不用一次性把 unittest.TestCase 全改成 def test_xxx(),新写的测试用 pytest 风格,老测试保持原样,逐步替换。
十一、一次完整的测试执行过程
前面把 fixture、parametrize、conftest、marker 拆开讲了,这里用一个完整案例把它们串起来。以下面这个项目结构为例:
calculator/
├── pyproject.toml # 注册 slow marker(见第九节)
├── conftest.py
├── src/
│ └── calc.py
└── tests/
├── conftest.py
└── test_operations.pypyproject.toml 里至少要包含 marker 注册,否则 @pytest.mark.slow 会触发 PytestUnknownMarkWarning:
[tool.pytest.ini_options]
markers = ["slow: 耗时较长的集成测试"]calculator/src/calc.py(被测代码):
class Calculator:
def __init__(self, db_url):
self.db_url = db_url
self._result = 0
def clear(self):
self._result = 0
def add(self, a, b):
self._result = a + b
return self._result
def multiply(self, a, b):
self._result = a * b
return self._result
def result(self):
return self._resultcalculator/conftest.py:
import pytest
@pytest.fixture(scope="session")
def db_url():
"""session 级:整个测试会话只需要一个数据库地址"""
return "sqlite:///:memory:"calculator/tests/conftest.py:
import pytest
from src.calc import Calculator
@pytest.fixture
def calc(db_url): # 依赖上层的 db_url
"""每个测试函数都拿到一个全新 Calculator 实例"""
c = Calculator(db_url)
c.clear()
yield c
c.clear()calculator/tests/test_operations.py:
import pytest
@pytest.mark.parametrize("a,b,expected", [
pytest.param(1, 2, 3, id="positive"),
pytest.param(-1, 5, 4, id="mixed"),
pytest.param(0, 0, 0, id="zero"),
])
def test_add(calc, a, b, expected):
assert calc.add(a, b) == expected
@pytest.mark.slow
def test_complex_calc(calc):
calc.add(1, 2)
calc.multiply(3, 4)
assert calc.result() == 12当运行 pytest 时,pytest 内部经历了以下步骤:
1. 测试发现
├── 扫描 tests/ 目录下匹配 test_*.py 的文件
├── 找到 test_operations.py
└── 从中收集 test_add 和 test_complex_calc
2. fixture 依赖解析
├── test_add(calc, a, b, expected) → 需要 fixture: calc
│ └── calc(db_url) → 需要 fixture: db_url
├── test_complex_calc(calc) → 需要 fixture: calc
│ └── same chain
└── 构建依赖图: db_url → calc → [test_add, test_complex_calc]
3. parametrize 展开
├── test_add 展开为 3 个独立用例:
│ ├── test_add[positive]
│ ├── test_add[mixed]
│ └── test_add[zero]
4. fixture setup(按拓扑序)
├── db_url (scope=session): 只运行一次,返回 "sqlite:///:memory:"
├── calc (scope=function): 每个测试函数运行一次
│ ├── test_add[positive]: setup calc → 测试 → teardown calc
│ ├── test_add[mixed]: setup calc → 测试 → teardown calc
│ ├── test_add[zero]: setup calc → 测试 → teardown calc
│ └── test_complex_calc: setup calc → 测试 → teardown calc
5. marker 筛选(如果指定了 -m)
└── pytest -m slow → 只跑 test_complex_calc
6. 报告输出
├── test_add[positive] . [25%]
├── test_add[mixed] . [50%]
├── test_add[zero] . [75%]
└── test_complex_calc . [100%]这个流程把 conftest 的层级作用域、fixture scope 的执行次数、parametrize 的展开和 marker 的过滤时机全部织在一起。排查"为什么某个 fixture 跑了太多次"或"为什么子目录的测试看不到这个 fixture"时,对照上面 6 步往回推。
十二、插件机制——pytest 的"长尾"
pytest 本身只做三件事:测试发现、fixture 调度、报告输出。其他能力(覆盖率、mock、并行、Django 集成、asyncio 集成)全部由插件完成。
一个最简插件就是带 hook 实现的 conftest.py:
# conftest.py
def pytest_runtest_makereport(item, call):
if call.when == "call":
print(f"[done] {item.nodeid}")更复杂的插件打包成 pytest-<name>,通过 pip install 安装,通过 entry_points 注册到 pytest。
pytest --trace-config 可以看到当前会话加载了哪些插件;pytest -p no:cacheprovider 可以临时禁用某个插件——这两条命令是排查"为什么测试突然变慢了 / 为什么某个 fixture 行为不对"的首选工具。
新人最容易踩的坑是"上来就装一堆插件"。先把 pytest 内置的 tmp_path / monkeypatch / capsys 用熟,再按需引入 pytest-mock(替代手写 mock)、pytest-cov(覆盖率)、pytest-xdist(并行)。其他插件多数是"特定框架的桥接"(Django/FastAPI/airflow 等),遇到再说。
十三、典型反模式与避坑建议
| 反模式 | 后果 | 修正 |
|---|---|---|
在 fixture 里做 import 大对象、scope=function | 每个测试都重导入,测试变慢 | 改成 scope="module" 或 session |
用 pytest.fail() 抛异常代替 assert | 失去 assert 反射信息 | 改用 assert 或 pytest 内置断言 |
测试里 time.sleep(N) 等异步事件 | 慢且不稳定 | 改用 monkeypatch / 事件注入 |
| 在 conftest.py 里写测试函数 | pytest 不会收集 | 测试放到 test_*.py 文件 |
| 测试间共享可变全局状态 | 后运行的测试受前面副作用影响 | 用 fixture scope 隔离 |
| 写超长参数列表的 parametrize | 失败信息像天书 | 用 pytest.param(..., id="...") 起可读名字 |
assert 后不写消息 | 失败时只看到"AssertionError" | assert x == y, "期望 y 因为 xxx" |
十四、常见问题与排查
测试一个都没跑起来。 先确认文件名和函数名符合发现规则:文件叫 test_*.py 或 *_test.py,函数以 test_ 开头。写进 conftest.py 的函数不会被收集,同理 Test 类里的非 test_ 方法也不会。用 pytest --collect-only 只看发现结果,能省去反复运行的时间。
报错 fixture 'xxx' not found。 多数是三种情况:参数名拼错;fixture 定义在别处(见第七节 conftest 可见性);或者想在同一个函数里引用参数化数据,却把 fixture 名也写进了 parametrize 的参数列表。前两种对照依赖关系排查,第三种看第五节和第六节的配合方式。
提示 PytestUnknownMarkWarning。 自定义 marker(如 @pytest.mark.slow)没有在配置里注册。第九节给了注册位置;警告本身不阻断执行,但意味着拼写错误的 marker 会静默失效——@pytest.mark.sloww 不会报错,只会被当成一个未知标签忽略,这是最常见的一种"测试没按预期跑"的原因。
测试之间互相影响。 症状是单个测试单独跑通过、整体跑挂掉。先怀疑可变全局状态:查哪些测试共享了同一个对象、模块变量或环境变量。修复方向按第五节 scope 的语义来——把共享资源收敛到合适 scope,或在 autouse fixture 里重置。session 级 fixture 只读 + 内部复制是稳妥做法。
想只看失败、不重跑全部。 pytest -x 在第一个失败处停住,适合改一处验证一处;pytest --maxfail=2 允许累计少量失败后停。CI 里想拿全量失败列表就都不要加,让一次运行报完。
慢测试难定位。 用 pytest --durations=10 列出耗时最长的 10 个测试。配合第三节的 --tb=short,能把"哪个用例慢 + 挂在哪一步"同时看清楚。定位到慢的 fixture 后,回到第五节按 scope 调整实例次数。
用例多、输出太长。 pytest -q 每条用例只打一个字符,失败再展开详情;-v 则是完整函数名。CI 日志用 -q 配合 --durations 通常比全量 -v 更可读。
十五、什么时候用 pytest,什么时候用别的
pytest 不是银弹,少数场景下别的工具更合适:
| 场景 | 推荐 |
|---|---|
| 纯标准库、小型工具 | unittest(标准库不需要额外依赖) |
| 纯文档示例验证 | doctest |
| 行为驱动(BDD,Given-When-Then) | pytest-bdd(保留 pytest 生态)或 behave |
| 大规模端到端(E2E) | Playwright / Cypress(pytest 只做编排) |
| 性能基准 | pytest-benchmark / locust(不要和功能测试混在一个 suite) |
| 强类型契约测试 | hypothesis(基于属性的测试,pytest 风格但独立库) |
十六、采用顺序建议
把 pytest 引入一个 Python 项目,按下面这个顺序铺:
- 第一周:装上
pytest,给现有unittest测试加上 pytest 跑通,CI 改成pytest。这一阶段不重写任何东西,只是切换工具链。 - 第二周:开始用
assert替代self.assertEqual,把简单的setUp改成@pytest.fixture。完成 30% 的迁移就够,剩下 70% 看团队节奏。 - 第一月:把跨文件共享的 setup 拆进
conftest.py,引入pytest-cov看覆盖率。 - 第二月:按业务需要引入
pytest-mock(替换 unittest.mock 的复杂语法)、pytest-xdist(并行执行,CI 时间减半)、业务框架对应的桥接插件(pytest-django/pytest-asyncio等)。 - 持续:自定义团队 marker(
smoke/regression/slow),写一份conftest.py编码规范,新人入职照着改。
十七、小结
- pytest 的入门成本是"一个
assert+ 一行pytest",不要被 1300+ 插件的生态规模吓到。 - 真正决定 pytest 能不能用好的是 fixture 体系:
yield分 setup/teardown、scope 控制生命周期、factory 模式做参数化资源、conftest.py 做跨文件命名空间。 - parametrize 是数据驱动测试的标配;marker 是测试分组的标配;
monkeypatch/tmp_path/capsys是副作用处理的三件套。 - pytest 兼容 unittest,老项目可以渐进式迁移,不需要一次性重写。
- 1300+ 插件是"按需引入",不是"全装上"——内置 fixture 才是 80% 场景的主力。
当你开始给 fixture 配 scope、往 conftest 分层、给用例打 marker,pytest 就不只是"跑测试的命令"了——它变成一套在测试会话层面做编排的工具。这几件事是在同一个层面发生的:scope 决定资源何时建、conftest 决定资源对谁可见、marker 决定用例按什么维度分组。把它们一起想,比单记某个 API 参数更容易用对。
参考链接
参与讨论
使用 GitHub 登录。欢迎补充事实、异议与实践。
讨论暂时无法加载。