🧪 第11章 建立单元测试
📄 一句话概括

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 秒实验

  1. 把 func 的 scope 改成 module,再跑第二节,看 function 那个数变成几。
  2. 把 only_here 从 tests/sub/conftest.py 移到项目根的 conftest.py,再跑第四节。
  3. 想一想:为什么 session 级的 fixture 有「互相污染」的风险?

常见误区

  1. 把造 app 的代码复制到每条测试里:改一次配置要改二十处。
  2. 该用 session 的用了 function:每条测试都重新加载模型,白等。
  3. 共用 session fixture 还往里写数据:顺序一换结果就变。
  4. conftest 放错层:fixture not found,而代码明明写着。
自测三问(点开对答案) 1. fixture 的三种作用域各建几次? —— function 每条测试一次、module 每个文件一次、session 整轮一次。 2. conftest.py 的可见范围是什么? —— 只往下传不往上走:项目根的全能看见,子目录的只有子目录能看见。 3. yield 之后的代码什么时候执行? —— 测试跑完之后,哪怕测试失败也执行。

工具齐了,下一篇真给应用写测试,再拉一份覆盖率出来。

相关笔记