bigywater.tech
Python · 37

pytest fixture:测试依赖是怎么自动注入的

真实用pytest -v -s跑module级和function级fixture的嵌套依赖,验证生命周期差异,以及测试函数为什么不需要import就能拿到fixture提供的值。

module级fixture
真实只setup一次,跨测试复用
function级fixture
真实每个测试独立setup/teardown
依赖注入方式
真实靠参数名字符串匹配,不需要import

做的事:一个module级fixture嵌套一个function级fixture

代码 test_fixture.py@pytest.fixture(scope="module") def db_conn(): print("db_conn: setup") yield "CONN_OBJ" print("db_conn: teardown") @pytest.fixture # 默认scope="function" def user(db_conn): print(f"user: setup (using {db_conn})") yield {"name": "alice"} print("user: teardown") def test_one(user): assert user["name"] == "alice" def test_two(user): assert user["name"] == "alice"

验证:db_conn真实只setup一次,user真实每个测试都重来

实测 · 本机执行输出(pytest -v -s)
test_fixture.py::test_one
  db_conn: setup                          ← 真实只在第一个测试前跑一次
  user: setup (using CONN_OBJ)
  test_one: got user={'name': 'alice'}
PASSED
  user: teardown                          ← function级,这个测试结束就teardown

test_fixture.py::test_two
  user: setup (using CONN_OBJ)            ← 真实又setup了一次(function级)
  test_two: got user={'name': 'alice'}
PASSED
  user: teardown
  db_conn: teardown                        ← module级,真实等到这个文件里所有测试都跑完才teardown

真实的生命周期完全由scope参数决定:db_conn声明的是scope="module",它的yield之前的代码真实只在整个模块第一次有测试用到它时执行一次,之后test_two再用到同一个db_conn,拿到的真实是同一份"CONN_OBJ",不会重新setup;user没写scope,默认是function级,真实每个测试函数都会重新setup一份全新的user对象,测试之间互不干扰——这也是为什么user依赖db_conn而不是相反:数据库连接这种"贵"的资源用模块级复用,具体的测试数据用函数级隔离,两种生命周期真实可以在同一棵依赖链条上自由组合。

验证:测试函数从没import这两个fixture,pytest真实是靠参数名字符串匹配

整份test_fixture.py里,test_one(user)和test_two(user)没有任何一行from ... import user——user这个名字真实只是测试函数签名里的一个普通参数。pytest收集测试的时候,真实会检查每个测试函数签名里的参数名列表,逐个拿这些名字去当前作用域(本文件、以及层层往上的conftest.py)里查找同名的@pytest.fixture函数,找到了就先把它的依赖链条(user依赖db_conn,所以db_conn真实会先被解析和执行)跑完,再把返回值(或者yield出来的值)真实塞进测试函数对应的参数位置去调用。这是一种基于命名约定的依赖注入:好处是测试函数的签名本身就是它需要哪些依赖的清单,代价是fixture名字一旦重复或者拼错,pytest真实只能在收集阶段报"fixture 'xxx' not found",编辑器很难在写代码的当下就发现这种基于字符串匹配的隐式关联。

验证:autouse=True真实让fixture不需要出现在参数列表里也会执行

实测 · 本机执行输出
@pytest.fixture(autouse=True)
def reset_state():
    print("reset_state: setup (autouse, no test asked for it)")
    yield
    print("reset_state: teardown")

def test_no_param():          # 参数列表里完全没有reset_state
    print("test_no_param: running")

输出:
reset_state: setup (autouse, no test asked for it)   ← 真实自动跑了,没人主动要求它
test_no_param: running
PASSED
reset_state: teardown

真实验证了autouse=True打破了"必须写在参数列表里才会被注入"这条默认规则——它让这个fixture对它所在作用域内(本文件、或者conftest.py所覆盖的整个目录)的每一个测试都自动生效,常见的真实用法是清理全局状态、重置mock、开关某个全局配置,这类"每个测试都该做但不值得每次都在签名里写一遍"的准备工作。

分步演示:从scope生命周期到参数名注入再到autouse

每一步都是本文上面用真实的pytest -v -s跑出来的原始结果。

fixture的生命周期和注入方式的真实规则
这一步新验证的规则

    本文用到的工具

    • 本机 Python 3.14.7 / pytest 9.1.1
    京ICP备2026067502号-1京公网安备11010502063487号