pytest-asyncio 共用 session fixture 报已被 torn down,检查循环工厂参数化
pytest-asyncio 1.4.0 中,循环工厂参数化可能让同步消费者拿到已终结的 session 异步 fixture。让消费者走一致的异步测试路径,并核对不同工厂与测试顺序。
两个测试单独执行都通过,先跑异步测试再跑使用同一 session fixture 的同步测试,却报:
AssertionError: The fixture value for "parent" is not available. This can happen when the fixture has already been torn down.
若项目使用 pytest-asyncio 1.4.0,并实现了 pytest_asyncio_loop_factories,要检查同步测试有没有跨过 loop factory 的参数化边界。这个组合的失败不一定是 fixture 自己关闭了资源。
问题出在两类测试获得了不同的参数化状态
已知场景中,parent 与依赖它的 child 都是 session 级异步 fixture。异步测试先请求 parent,同步测试随后请求 child。前者经过 loop factory 参数化,后者没有,pytest 因而可能切换相关 fixture 的缓存状态,已经准备的 parent 被提前终结。
1.4.0 的正式插件在生成 loop factory 参数时会先判断测试是否属于异步测试;普通 def 测试直接跳过。即便 hook 只返回一个 asyncio.new_event_loop,也不能据此认为它与完全没有 hook 等价。
这与 strict 模式不接管普通 @pytest.fixture 定义的异步 fixture 不同:这里使用的是正确的 @pytest_asyncio.fixture,资源也已经生成,失败发生在后续生命周期切换。
让共享异步资源的消费者走同一条路径
测试本来就围绕异步资源工作时,可把这个同步消费者改成异步测试,并明确使用相同的 session 循环。下面的字典只是资源占位,项目中应保留原有的异步准备与关闭逻辑:
import pytest
import pytest_asyncio
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def parent():
yield {"ready": True}
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def child(parent):
yield parent
@pytest.mark.asyncio(loop_scope="session")
async def test_parent(parent):
assert parent["ready"]
@pytest.mark.asyncio(loop_scope="session")
async def test_child(child):
assert child["ready"]
这里没有为了语法形式添加无意义的 await;异步声明是让插件将消费者纳入相同的循环工厂选择。项目如果确实使用多种 loop factory,各消费者还需选择一致的工厂集合,并检查每组参数各自的准备和终结次数。
如果 hook 只是返回默认工厂、项目不需要循环工厂矩阵,可以评估移除这个 hook,使用插件默认循环选择。若原本要覆盖 uvloop 等多种实现,删 hook 就会减少测试覆盖,不能把覆盖变少后的绿色结果当成完整修复。
不要靠交换测试顺序、过滤这个 AssertionError,或在 fixture 内手工重建已经终结的上游对象。它们都会让生命周期依赖继续隐藏。保持 scope 与 loop_scope 的长度相容,也不要重新添加已被移除的旧 event_loop fixture。
修改后按两种测试顺序检查,并记录资源何时创建、何时关闭。每个工厂下都应在最后一个消费者结束后再关闭资源;只运行原先失败的单个测试,无法覆盖这个切换条件。正式 1.4.0 仍有上述参数化入口边界,使用其他版本时应核对实际实现。


