真实用pytest -v -s跑module级和function级fixture的嵌套依赖,验证生命周期差异,以及测试函数为什么不需要import就能拿到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"
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而不是相反:数据库连接这种"贵"的资源用模块级复用,具体的测试数据用函数级隔离,两种生命周期真实可以在同一棵依赖链条上自由组合。
@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、开关某个全局配置,这类"每个测试都该做但不值得每次都在签名里写一遍"的准备工作。
每一步都是本文上面用真实的pytest -v -s跑出来的原始结果。