写异步测试的时候,最怕什么?不是逻辑复杂,而是明明写了 async def test_xxx,pytest 跑完显示“passed”,但实际什么都没测——只因为忘了加 await,或者 pytest 根本不认识它。今天就把这块的坑和正确操作一次性说清楚,省得你对着 RuntimeWarning 发呆。
先看最常见的报错场景:pytest 运行 async test 时弹出 RuntimeWarning: coroutine 'test_xxx' was never awaited。说白了,pytest 默认不认 async def 函数,你定义了一个协程,它却把它当普通函数调,结果协程对象被创建后没被 await,Python 自然就抛了警告,而测试还显示“passed”——实际上里面一行代码都没跑。
解决路径只有一条:让 pytest 知道该用 event loop 来跑它。别自己去写 loop.run_until_complete,交给插件处理更稳。
- 装
pytest-asyncio:pip install pytest-asyncio - 在项目根目录加
pytest.ini或pyproject.toml,显式启用插件(新版本 pytest 不再自动发现) - 配置里必须指定
asyncio_mode = auto,否则默认是strict,遇到没标@pytest.mark.asyncio的 async test 会跳过或报错

async test 函数必须加 @pytest.mark.asyncio 吗
不一定,但强烈建议加。不加的前提是:你用了 asyncio_mode = auto,且函数名符合 pytest 默认匹配规则(比如 test_*.py 里的 async def test_*)。
问题在于“auto”模式有边界:它只对模块级 async test 生效;如果 test 在 class 里、或函数带 fixture(尤其 session/scoped fixture),不加 mark 极大概率静默失败或 loop 复用出错。
- class 内的 async test 必须加
@pytest.mark.asyncio,否则 pytest 直接忽略 - 用了
event_loopfixture(比如要手动控制 loop)时,必须加 mark,否则 fixture 注入失败 - 加了 mark 后,插件会确保每个 test 用干净的 event loop 实例,避免状态污染
一句话:但凡你拿不准,先把 mark 加上,总比 debug 半天强。
测试中调用 asyncio.sleep() 或真实 IO 导致测试变慢
异步测试慢,往往不是因为 asyncio 本身,而是你在 test 里真发了 HTTP 请求、连了 DB、或用了 asyncio.sleep(1) 模拟延迟——这会让单个 test 卡 1 秒,批量跑就不可接受。
正确做法是 mock 掉耗时协程,而不是降速跑真实逻辑。
- 用
unittest.mock.AsyncMock替换依赖的 async 方法(Python 3.8+),例如:mock_obj.fetch_data = AsyncMock(return_value={"ok": True}) - 避免用
patch去 mock 整个 module,容易漏掉 import 路径;优先 patch 具体被测函数里 import 的位置(比如my_module.aioclient.get,而不是aiohttp.ClientSession.get) - 如果非得测真实 sleep 行为(比如验证重试逻辑),改用
asyncio.sleep(0)触发调度,或用time.perf_counter()断言耗时范围,别硬等
fixture 返回 async 对象时,yield 和 return 怎么选
async fixture 不能用 yield,因为 yield 语句无法在 async def 里直接用 —— 你会看到 SyntaxError: 'yield' inside async function。
正确方式是用 async def + return,清理逻辑单独写 teardown 函数,或用 async with 管理生命周期。
- 简单资源(如临时数据库连接):直接
returnconnection,teardown 放在 test 结尾手动await conn.close() - 需要自动清理:定义一个 async context manager,然后在 fixture 里
async with MyResource() as r: yield r—— 注意这里 yield 在 async with 块内,外层仍是async def - 别试图在 sync fixture 里
await,pytest 不允许;所有 async fixture 必须声明为@pytest.fixture(scope="...", autouse=True)并标记@pytest.mark.asyncio
最常被忽略的是 event loop 的 scope。function 级 test 默认用独立 loop,但如果你写了 session-scoped async fixture,多个 test 共享同一个 loop,中间任何未 await 的协程残留都会导致后续 test 报 RuntimeError: Event loop is closed——这种问题不会立刻暴露,得看 CI 上的偶发失败。所以,scope 的配置一定要和你的实际资源生命周期对齐,别偷懒。