ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用登录状态讲清Pytest fixture:依赖、scope与yield

用登录状态讲清Pytest fixture:依赖、scope与yield

当自动化测试开始连接账号、数据库、接口或浏览器后,测试函数往往还没写几行,前置准备和结束清理就已经占了大半:先创建账号,再完成登录;测试结束后退出会话、删除账号,最后关闭连接。如果每条用例都重复这套过程,不仅代码冗长,清理遗漏还会让后面的用例受到脏数据影响。

pytest fixture解决的正是这类资源组织问题。它不只是把几行前置代码抽成函数,更重要的是建立一套明确的依赖关系和生命周期。本文用账号存储、注册用户和登录客户端串起一条完整链路,重点说明fixture依赖、yield清理、scope选择以及常见的ScopeMismatch错误。

一、先看一个真实的登录测试依赖

一条“已登录用户可以查看个人资料”的测试,表面上只需要一个登录后的客户端,实际依赖至少包括:

  • 一份密码规则;

  • 一个可写入账号的存储;

  • 一个已经注册的用户;

  • 一个完成登录的客户端;

  • 测试结束后的退出、删号和存储关闭动作。

如果把这些步骤全写进测试函数,断言会被大量准备代码淹没。fixture允许测试只声明最终需要的状态,其余资源由pytest沿依赖关系逐层准备:

对应的测试函数很短:

def test_logged_in_user_can_view_profile( authenticated_client: SessionClient, ) -> None: profile = authenticated_client.get_profile() ​ assert profile == {"username": "fixture_user", "status": "active"}

这里没有手动调用authenticated_client()。测试函数参数就是对fixture的请求,pytest根据参数名找到同名fixture,再继续解析它依赖的其他fixture。测试代码只关注操作和预期,环境准备则集中在fixture中维护。

二、用conftest.py组织公共fixture

本文把公共fixture放在项目根目录的conftest.py中。该目录及其子目录里的测试都可以直接使用这些fixture,不需要再写导入语句。

from collections.abc import Iterator ​ import pytest ​ from account_service import AccountStore, Credentials, PasswordPolicy, SessionClient ​ ​ @pytest.fixture(scope="session") def password_policy() -> PasswordPolicy: return PasswordPolicy(minimum_length=8) ​ ​ @pytest.fixture def account_store() -> Iterator[AccountStore]: store = AccountStore() yield store store.close()

password_policy是一份只读规则,整次测试运行期间没有可变状态,因此使用session作用域。account_store保存账号数据,为了避免不同测试之间相互影响,每条测试都应获得一个独立且初始为空的存储实例,所以保留默认的function作用域。

在此基础上,可以通过函数参数声明fixture之间的依赖关系:

@pytest.fixture def registered_user( account_store: AccountStore, password_policy: PasswordPolicy, ) -> Iterator[Credentials]: credentials = Credentials( username="fixture_user", password="safe-pass-2026", ) account_store.create_user(credentials, password_policy) yield credentials account_store.delete_user(credentials.username) ​ ​ @pytest.fixture def authenticated_client( account_store: AccountStore, registered_user: Credentials, ) -> Iterator[SessionClient]: client = SessionClient(account_store) client.login(registered_user) yield client client.logout()

registered_user声明自己需要账号存储和密码规则;authenticated_client又声明需要账号存储和注册用户。pytest会先完成依赖,再执行当前fixture。这样拆分后,其他测试也可以只请求account_storeregistered_user,不必为了复用账号数据而被迫建立登录会话。

conftest.py适合放测试目录共享的fixture,但不适合逐渐变成什么都装的工具文件。普通业务辅助函数仍应放在独立模块中;fixture只负责提供测试所需的状态和资源边界。

三、yield前后分别发生了什么

yield fixture可以把资源的创建和清理写在一起:yield之前是准备阶段,yield产生的对象交给测试使用,测试结束后再回到yield之后执行清理。

对登录场景运行下面的命令,可以直接观察setup和teardown顺序:

.venv\Scripts\python -m pytest tests/test_account_access.py::test_logged_in_user_can_view_profile --setup-show -q

本次运行的关键输出为:

SETUP S password_policy SETUP F account_store SETUP F registered_user SETUP F authenticated_client test_logged_in_user_can_view_profile . TEARDOWN F authenticated_client TEARDOWN F registered_user TEARDOWN F account_store TEARDOWN S password_policy

准备阶段沿依赖关系向内展开:存储准备好后创建账号,账号存在后才能登录。清理阶段则按相反顺序退出:先退出会话,再删除账号,最后关闭存储。这种反序不是偶然,后创建的资源通常依赖先创建的资源,也应该先释放。

正常的断言失败不会跳过已经成功建立的fixture清理。例如测试函数在yield之后拿到客户端,即使后面的assert失败,pytest仍会继续执行退出、删号和关闭存储。

但有一个边界需要区分:如果fixture在到达yield之前就抛出异常,那么该fixture写在yield之后的代码不会执行。此前已经成功建立的其他fixture仍会进入各自的清理阶段。为了降低资源残留风险,比较稳妥的做法是让每个fixture只承担一个会改变状态的动作,并紧接着yield:创建账号和删除账号放在一起,登录和退出登录放在一起,不要用一个fixture连续创建多种外部资源后才交给测试。

四、scope不是单纯的性能选项

fixture的scope决定一个实例在多大范围内复用,也决定它什么时候销毁。pytest提供五种常用作用域:

scope复用范围常见选择
function每条测试创建一次账号数据、独立客户端、可变对象
class一个测试类共用同一测试类内部共享的状态
module一个测试模块共用模块级连接或固定资源
package一个测试包共用包级服务或环境资源
session整次测试运行共用只读配置、创建成本较高且可安全共享的服务

作用域越宽,创建次数通常越少,但共享状态的范围也越大。因此不能因为登录一次更快,就直接把账号、客户端和会话全部改成session。如果某条测试修改了用户资料、权限或会话状态,后续测试可能读到前一条测试留下的结果,单独运行正常,批量运行却不稳定。

本文只把不可变的密码规则设为session,账号存储、注册用户和登录客户端均使用function。运行下面这条隔离性测试时,account_store.user_count应始终为0:

def test_each_test_gets_an_empty_store(account_store: AccountStore) -> None: assert account_store.user_count == 0

这条断言看似简单,却明确检查了一个关键约束:每条测试都从干净状态开始。选择作用域时,应该先回答“这个状态能否安全共享”,再考虑减少多少创建成本。

五、复现ScopeMismatch:宽作用域不能依赖窄作用域

把一个共享客户端设为module,同时让它依赖默认function作用域的登录客户端:

@pytest.fixture(scope="module") def shared_client( authenticated_client: SessionClient, ) -> SessionClient: return authenticated_client

显式运行该文件:

.venv\Scripts\python -m pytest examples/test_scope_mismatch.py -q

pytest在测试函数执行前就会报错:

ScopeMismatch: You tried to access the function scoped fixture authenticated_client with a module scoped request object. 1 error in 0.02s

原因在于生命周期无法成立:shared_client希望整个模块只创建一次,而它依赖的authenticated_client却应该在每条测试后销毁。一个存活时间更长的资源,不能持有已经按更短周期清理掉的依赖。

这类问题有两种处理方向:

  1. 如果登录状态会变化,把shared_client也改成function,让每条测试独立登录和退出。这通常是更稳妥的选择。

  2. 只有确认整条资源链都适合共享时,才把被依赖的fixture一并调整为更宽的作用域。不能只改最外层fixture;账号存储、注册用户和客户端的生命周期必须相互兼容。

本次正常测试集和错误场景分别运行,结果如下:

这里的ScopeMismatch属于setup error,而不是测试断言失败。它说明测试尚未进入执行阶段,fixture依赖图就已经无法建立。

六、autouse应该少而明确

给fixture添加autouse=True后,作用域内的测试即使没有声明参数,也会自动使用它。它适合真正无条件的公共行为,例如每条测试前重置固定的全局开关,或者统一恢复某个进程内状态。

登录用户通常不适合设成autouse。匿名访问测试本来不需要账号和会话,如果它也被自动登录,不仅增加准备时间,还会改变测试前提。更麻烦的是,读测试函数时看不到这层依赖,定位问题必须回头搜索上级目录里的conftest.py

判断是否使用autouse,可以先问两个问题:作用域内是不是每一条测试都必须执行它?省略这个依赖是否会让测试意图更难理解?只要答案不够确定,就优先使用显式参数。

七、几个容易留下脏状态的写法

1. 一个fixture包办所有准备工作

一次完成建库、建号、授权、登录和数据写入,看起来调用方便,但其中一步失败时,很难判断已经创建了哪些资源,也不容易保证清理完整。按资源边界拆分fixture,依赖关系会更长,却更容易定位失败和安排反向清理。

2. 只在整次测试结束后统一删数据

如果账号数据使用session作用域统一清理,中途的测试会共享越来越多的状态。测试进程提前终止时,最后的清理还可能没有机会运行。外部数据库中的测试数据最好同时具备唯一标识、幂等删除或定期回收机制,不能把所有保障都压在teardown上。

3. 为了提速随意扩大scope

作用域扩大后,速度可能提升,但隔离性也随之下降。浏览器、数据库连接等资源可以考虑复用底层连接,而账号、购物车、订单这类业务状态仍尽量按测试隔离。资源连接和业务数据不一定要使用相同的作用域。

4. 在测试文件中直接导入fixture

conftest.py中的fixture由pytest按目录发现,测试通过参数请求即可。直接从conftest导入会把pytest的依赖注入又写成普通函数调用关系,也容易让目录层级和可见范围变得混乱。

5. 认为teardown在任何情况下都会执行

pytest会尽力清理已经成功建立的fixture,但进程被强制终止、机器断电或外部服务失联时,Python代码可能根本没有执行机会。真实外部资源还需要服务端过期策略、唯一数据前缀和可重复执行的清理工具作为补充。

八、小结与思考

fixture真正有价值的地方,不是少写几个初始化函数,而是让测试资源形成一张可读的依赖图:测试声明自己需要什么,pytest按依赖准备资源,再按反序释放资源。

在本文的登录链路中,只读密码规则可以在整次运行中共享;账号存储、注册用户和登录客户端则按测试隔离。yield把创建与清理放在同一个fixture里,--setup-show可以直接观察执行顺序,而ScopeMismatch会阻止不兼容的生命周期组合。

当fixture越来越多时,可以持续检查三个问题:每个fixture是否只负责一种清晰状态,作用域是否与共享风险匹配,清理是否紧跟资源创建并且允许重复执行。把这三个边界守住,测试数量增加后,前置条件和数据清理仍然能够保持可控。

参考资料

  • pytest:How to use fixtures

  • pytest:Fixtures reference

  • pytest:About fixtures

  • pytest PyPI

本文代码

GitHub:004-pytest-fixtures

返回列表