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
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 时的事件循环管理问题:
- 测试阶段:使用 session 级别的事件循环
- Teardown 阶段:pytest-asyncio 尝试在不同的事件循环中清理资源
- 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 错误可以忽略。
最佳实践
- 关注测试结果:只看
passed/failed/skipped统计 - 忽略 teardown 错误:这是框架限制,不是代码问题
- 保持 session 级别 engine:性能最优
- 定期更新 pytest-asyncio:新版本可能修复此问题