fixture 是依赖注入:测试声明「我需要什么」,pytest 负责造。 三种作用域实测:function 建 4 次、module 2 次、session 1 次。 conftest 只往下传不往上走,放错层就是 fixture not found。
这一篇对应教材 11.2 与 11.2.1:pytest 的 fixture 夹具、以 conftest.py 扩充 fixture。 上一篇的测试都是自己造数据,这一篇把「造数据」这件事抽出来。
一、fixture 是什么
@pytest.fixture
def func():
return "一个准备好的值"
def test_one(func): # 参数名对上 fixture 名就能拿到
assert func
没人调用它,pytest 自己填。测试函数的参数名等于 fixture 的名字,就有了。
这叫依赖注入:测试只说「我需要一个 app」,不管它是怎么造的、用完要不要清。
跟第 2 章那个 create_app() 工厂是同一个思路——把「怎么造」和「怎么用」分开,
只是这一次的使用者从端点换成了测试。
好处很实在:造 app 要十几行,写二十条测试就是二百多行重复代码。而且哪天配置改了,二十处都得改。
二、三种作用域,各建了几次
| 作用域 | 建几次 | 用在哪 |
|---|---|---|
function(默认) |
每条测试一次 | 绝大多数情况 |
module |
每个文件一次 | 文件内共享的只读数据 |
session |
整轮一次 | 数据库、模型加载、起服务 |
实测:4 条测试分在两个文件里,function 建了 4 次、module 2 次、session 1 次。 function 隔离最彻底——每条测试都拿到全新的对象,改坏了也不影响下一条。代价是慢。 session 只建一次,数据库初始化、加载模型这种活儿就该放这里。 第 8 章那个检测器加载一次要 35 毫秒,一百条测试就是 3.5 秒,全浪费在重复加载上。
但 session 有个坑:它是共享的。一条测试改坏了里面的状态,后面全受影响。 顺序一换结果就变,那种测试比没有还糟。
三、conftest.py 只往下传
| 文件位置 | 谁看得见 |
|---|---|
项目根 conftest.py |
所有子目录里的测试 |
tests/conftest.py |
tests/ 以及它下面的所有目录 |
tests/sub/conftest.py |
只有 sub/ 里面 |
实测把 fixture 写在项目根,测试在 tests/ 下,照样能用——根目录在搜索路径上。
反过来不行。
实测 tests/sub/ 里定义的 only_here,在 tests/test_d.py 里引用,
报 fixture 'only_here' not found。
规则是「只往下传,不往上走」。
这条错在真实项目里很常见:把 fixture 放进某个子目录的 conftest,然后在另一个目录里写测试, 跑起来才发现「明明写过」。 判断放哪儿的办法: 只有一处用 → 就地放在那个测试文件的顶部;一个目录用 → 那个目录的 conftest;全项目用 → 项目根。
四、yield:用完要收摊
@pytest.fixture
def box():
print("[fix] 开箱")
yield {"n": 0}
print("[fix] 收箱")
实测输出是「开箱 → 测试跑 → 收箱」,顺序清清楚楚。
yield 之前是准备,之后是清理。测试跑完自动执行后面那半段,哪怕测试挂了也执行。
这一点比写在测试函数末尾强:断言一挂,函数后面的代码就跳过了,临时文件、数据库连接、
起过的服务全留在那儿。跑一百条测试能攒出一堆垃圾。
第 11.3 节那个 app fixture 就是这么写的:开一个临时目录,yield 出去,最后 rmtree 掉。
五、fixture 能提供状态,不只是数据
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def auth(client):
client.get("/login-as/1")
return client
fixture 的参数也可以写别的 fixture 名,pytest 按依赖顺序造。
auth 拿到的是那个已经登入的 client。测试里就不用每条都写一遍登入。
这说明 fixture 不只能给数据,也能给状态:登入态、开好的事务、打好了补丁的配置、
已经切到测试库的连接。
第 11.3 节那八条测试,有五条用的是 auth,三条用的是 client。
「没登入会怎样」和「登入之后会怎样」,各用各的。
六、动手 Demo:fixture:依赖注入、作用域与 conftest
这个 Demo 用一个计数器把三种作用域各建了几次数出来,再把 conftest 放错层试试。 第二节那三个数(1/2/4)是这一篇的核心实测。
@pytest.fixture(scope="session")
def sess():
return bump("session")
@pytest.fixture(scope="module")
def mod():
return bump("module")
@pytest.fixture
def func():
return bump("function")
实跑输出:
--------------------------------------------------------------------
fixture:依赖注入、作用域与 conftest
--------------------------------------------------------------------
① fixture 是什么
--------------------------------------------------------------------
[形状]
| @pytest.fixture
| def func():
| return "一个准备好的值"
|
| def test_one(func): # 参数名对上 fixture 名就能拿到
| assert func
测试函数的参数名,等于 fixture 的名字。没人调用它,pytest 自己填。
! 这叫依赖注入:测试只说「我需要一个 app」,不管它是怎么造的、用不用清。
造的活儿归 fixture,测试只关心行为。
! 跟第 2 章那个 create_app() 工厂是同一个思路,只是换了个使用者。
② 三种作用域,各建了几次
--------------------------------------------------------------------
4 条测试,返回码 0
session 建了 1 次 / module 建了 2 次 / function 建了 4 次
! 默认是 function——每条测试都重新建一次,四条测试就是四次。隔离最彻底,代价是慢。
! module 是「一个文件建一次」,4 条测试分在两个文件里,所以 2 次。
! session 是「整轮测试建一次」,所以 1 次。数据库初始化、加载模型这类活儿就放这里。
③ conftest.py 在哪一层,谁能用
--------------------------------------------------------------------
| 文件位置 | 谁看得见 |
|---|---|
| 项目根 `conftest.py` | 所有子目录里的测试 |
| `tests/conftest.py` | `tests/` 以及它下面的所有目录 |
| `tests/sub/conftest.py` | 只有 `sub/` 里面 |
! 刚才那三条 fixture 都写在项目根的 conftest.py 里,而测试在 tests/ 下——照样能用,
因为根目录在搜索路径上。
! 反过来不行:tests/sub/conftest.py 里的 fixture,tests/test_a.py 拿不到。
规则是「只往下传,不往上走」。
④ 子目录的 conftest 只影响子目录
--------------------------------------------------------------------
返回码 1
_____________ ERROR at setup of test_root_cannot_use_sub_fixture ______________
def test_root_cannot_use_sub_fixture(only_here):
E fixture 'only_here' not found
> available fixtures: anyio_backend, anyio_backend_name, anyio_backend_options, capfd, c
> use 'pytest --fixtures [testpath]' for help on them.
ERROR tests/test_d.py::test_root_cannot_use_sub_fixture
! tests/sub/test_c.py 用得着 only_here,tests/test_d.py 拿不到,
报 fixture 'only_here' not found。
! 这条错在真实项目里很常见:把 fixture 放进某个子目录的 conftest,然后在另一个目录里写测试,
跑起来才发现「明明写过」。
⑤ yield:用完要收摊
--------------------------------------------------------------------
返回码 0
[fix] 开箱
[fix] 收箱
1 passed in 0.01s
! yield 之前是准备,之后是清理。测试跑完自动执行后面那半段——哪怕测试挂了也执行。
! 清理放这儿最稳:临时文件、数据库连接、启动的服务。
写在测试函数末尾的话,断言一挂就跳过了,脏东西留在盘上。
第二节三个数:session 1、module 2、function 4。文件有两个,所以 module 是 2。 第四节那条 fixture not found 是 conftest 放错层的典型报错。 第五节「开箱」「收箱」两行,中间夹着测试的执行。
30 秒实验
- 把
func的 scope 改成module,再跑第二节,看 function 那个数变成几。 - 把
only_here从tests/sub/conftest.py移到项目根的 conftest.py,再跑第四节。 - 想一想:为什么 session 级的 fixture 有「互相污染」的风险?
常见误区
- 把造 app 的代码复制到每条测试里:改一次配置要改二十处。
- 该用 session 的用了 function:每条测试都重新加载模型,白等。
- 共用 session fixture 还往里写数据:顺序一换结果就变。
- conftest 放错层:fixture not found,而代码明明写着。
自测三问(点开对答案)
1. fixture 的三种作用域各建几次? —— function 每条测试一次、module 每个文件一次、session 整轮一次。 2. conftest.py 的可见范围是什么? —— 只往下传不往上走:项目根的全能看见,子目录的只有子目录能看见。 3. yield 之后的代码什么时候执行? —— 测试跑完之后,哪怕测试失败也执行。工具齐了,下一篇真给应用写测试,再拉一份覆盖率出来。
- ☐ (本篇完,下一篇 11.3 给应用写测试并输出覆盖率)