pytest 异步 fixture 变成 async_generator:检查 strict 模式和装饰器
async fixture 返回生成器或协程对象时,检查 pytest-asyncio strict 模式与装饰器。区分旧版本症状、新版警告或错误,明确 auto 模式的接管范围及循环生命周期。
异步测试收到的 fixture 本该是客户端或资源对象,却出现 AttributeError: 'async_generator' object has no attribute;没有 yield 的 async fixture 也可能暴露协程对象,随后提示未等待。若 fixture 写成 @pytest.fixture,而 pytest-asyncio 使用 strict 模式,应先检查插件有没有接管它。
这是常见的历史表现,新版组合也可能在 setup 阶段先发出弃用警告或直接报错。不能要求所有版本都先得到 async_generator。以下配置与示例以 pytest-asyncio 1.4.0 为核对基准;该版本要求 Python 3.10 及以上、pytest 8.4 至 9.x。旧环境应先核对依赖兼容,不能直接照抄升级版本。
测试的 asyncio 标记不会替 fixture 改装饰器
strict 模式只处理带 @pytest.mark.asyncio 的测试,以及用 @pytest_asyncio.fixture 标记的异步 fixture。这两个声明分别作用于测试和 fixture。只给测试加标记,普通 async @pytest.fixture 仍不会因此自动交给 pytest-asyncio。
因此下面这种组合有问题:
import pytest
@pytest.fixture
async def resource():
yield {"ready": True}
@pytest.mark.asyncio
async def test_resource(resource):
assert resource["ready"] is True
在保留 strict 的项目中,替换 fixture 的装饰器,并导入提供它的模块。下面是完整的最小示例,字典仅表示资源,不涉及网络或数据库:
import pytest
import pytest_asyncio
@pytest_asyncio.fixture(loop_scope="function")
async def resource():
value = {"ready": True}
try:
yield value
finally:
value.clear()
@pytest.mark.asyncio(loop_scope="function")
async def test_resource(resource):
assert resource["ready"] is True
fixture 默认缓存范围是 function。这里显式让 fixture 和测试都使用 function 事件循环。插件运行到 yield 后,把 value 交给测试;测试结束后再继续 fixture 的清理段。实际资源的异步关闭应放在相应清理逻辑中,并遵守资源库的关闭约定。
测试应直接使用已准备好的 resource。不要写 await resource 去补偿错误装饰器:异步生成器对象不是可直接等待的协程,准备后的字典也不是 awaitable。手工调用 resource() 或自行取一次 __anext__() 还会绕开 fixture 注入和 teardown 管理。
只有 asyncio 的项目,可以明确选用 auto
若整个测试项目只使用 asyncio,auto 模式可以自动标记异步测试,也会接管普通 @pytest.fixture 定义的 async fixture。可以把下面配置合并到现有 pytest.ini,保留其他设置:
[pytest]
asyncio_mode = auto
采用这个方案后,前面普通 @pytest.fixture 的声明可以由插件处理;已有 @pytest_asyncio.fixture 也可以保留。选择一种清楚的项目约定,比不同文件依赖隐含模式更容易维护。
同时使用 Trio、AnyIO 等其他异步测试机制时,不宜直接把全项目切到 auto。它会取得所有异步 fixture 的处理权,可能接管原本属于其他机制的 fixture。保留 strict,并明确每个测试及资源由谁运行。
核对实际测试进程使用的解释器、已加载的插件、配置文件和命令行参数。命令行 --asyncio-mode 可以覆盖配置;安装在另一个虚拟环境里的插件,对当前进程没有作用。插件被禁用、没有加载,或 fixture 名称被其他定义覆盖时,仅改一个配置文件也可能碰不到当前对象。
升级后警告或报错,要按版本判断
pytest-asyncio 0.19 起默认模式改为 strict,0.20 移除了 legacy。依赖升级后才出现症状,可能是旧项目一直依赖普通 async fixture 的自动处理,却没有显式声明模式。
0.25 起对 asyncio 测试请求 strict 模式下普通 async @pytest.fixture 的情况增加弃用提醒。pytest 自身也逐步收紧未被插件处理的异步 fixture:8.4 引入相关弃用,9.0 移除这项支持。因此实际表现取决于 pytest、pytest-asyncio 的组合以及警告是否被当成错误处理。修复装饰器和模式归属,不用过滤警告维持旧行为。
改为正确装饰器后若出现跨事件循环或 ScopeMismatch,再检查 scope 和 loop_scope。缓存资源的范围与运行它的循环范围不同;循环范围必须至少与 fixture 缓存范围一样长。本文最小示例都用 function,不能把它改成 session 缓存后仍保留 function 循环。新版循环选择使用公开的 loop_scope;pytest-asyncio 1.0 已移除旧 event_loop fixture,不要复制重定义它的旧方案。
还要核对 fixture 真正提供的返回值。即使装饰器正确,return some_async_generator() 仍可能有意返回生成器对象;插件不会把任意返回对象自动展开成业务资源。这与本文的“异步 fixture 没被接管”是另一种情况。
修复后按原测试入口确认收到的是预期资源,setup 与 teardown 都执行,清理异常也能使测试任务失败。正常测试和断言失败的路径都应检查;只看到断言通过,不能证明资源已释放。需要回退时恢复依赖锁定、模式配置和 fixture 声明的整组变化,避免用不兼容的声明搭配旧环境。


