DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

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 声明的整组变化,避免用不兼容的声明搭配旧环境。