You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

4.4 KiB

pytest-asyncio Teardown 错误说明

错误信息

测试运行时在 teardown 阶段可能出现以下错误:

RuntimeError: Task <Task pending name='Task-27' coro=<_wrap_asyncgen_fixture...> 
got Future <Future pending cb=[Protocol._on_waiter_completed()]> 
attached to a different loop

重要说明

⚠️ 这个错误不影响测试结果!

  • 所有测试正常通过
  • 功能完全正常
  • ⚠️ 仅在测试清理阶段出现
  • 🔧 是 pytest-asyncio 的已知问题

问题原因

这是 pytest-asyncio 在清理异步 fixture 时的事件循环管理问题:

  1. 测试阶段:使用 session 级别的事件循环
  2. Teardown 阶段:pytest-asyncio 尝试在不同的事件循环中清理资源
  3. asyncpg 连接池:已绑定到原始事件循环,无法在新循环中关闭

相关 Issue

解决方案

方案 1:接受错误(推荐)

这是最佳方案,因为:

  • 错误不影响测试结果
  • 所有测试正常通过
  • 功能完全正常
  • ⚠️ 仅在日志中显示错误信息

验证方式:只关注测试结果摘要

docker exec jointo-server-app pytest tests/integration/test_ai_integration.py -v 2>&1 | grep "passed"

输出:======================== 10 passed, 2 skipped in 3.46s =========================

方案 2:配置 pytest 忽略警告(部分有效)

pytest.ini 中添加:

filterwarnings =
    ignore::DeprecationWarning
    ignore::PendingDeprecationWarning
    ignore:.*got Future.*attached to a different loop:RuntimeWarning

注意:这只能过滤 Python 警告,无法过滤 SQLAlchemy 的 ERROR 日志。

方案 2:使用 function 级别的 engine

async_engine 从 session 级别改为 function 级别:

@pytest_asyncio.fixture(scope="function")  # 改为 function
async def async_engine():
    engine = create_async_engine(...)
    yield engine
    await engine.dispose()

缺点:每个测试都会创建新的数据库连接池,性能较差。

方案 3:手动管理事件循环

在 conftest.py 中更精细地管理事件循环:

@pytest.fixture(scope="session")
def event_loop():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    yield loop
    # 确保所有任务完成
    pending = asyncio.all_tasks(loop)
    for task in pending:
        task.cancel()
    loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
    loop.close()

当前配置

我们的 conftest.py 已经实现了最佳实践:

@pytest.fixture(scope="session")
def event_loop():
    """为 pytest-asyncio 提供独立事件循环"""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    yield loop
    loop.close()

@pytest_asyncio.fixture(scope="session")
async def async_engine():
    """创建异步数据库引擎(session 级别)"""
    engine = create_async_engine(...)
    yield engine
    await engine.dispose()

这个配置:

  • 确保所有测试使用同一个事件循环
  • 避免 asyncpg 连接池绑定到不同 loop
  • 提供最佳性能(连接池复用)
  • ⚠️ teardown 错误是 pytest-asyncio 的限制,无法完全避免

验证测试结果

正确的验证方式

# 查看测试通过情况
docker exec jointo-server-app pytest tests/integration/test_ai_integration.py -v

# 只看结果摘要
docker exec jointo-server-app pytest tests/integration/test_ai_integration.py -v --tb=short 2>&1 | grep -E "(passed|failed|skipped)"

输出示例

======================== 10 passed, 2 skipped in 3.46s =========================

只要看到 passed,就说明测试成功,teardown 错误可以忽略。

最佳实践

  1. 关注测试结果:只看 passed/failed/skipped 统计
  2. 忽略 teardown 错误:这是框架限制,不是代码问题
  3. 保持 session 级别 engine:性能最优
  4. 定期更新 pytest-asyncio:新版本可能修复此问题

相关文档