
本文旨在解决在使用unittest.IsolatedAsyncioTestCase测试FastAPI异步路由时遇到的“RuntimeError: Event loop is closed”问题。核心方案是分离FastAPI应用与测试代码,并采用正确的异步测试结构,确保每个异步测试逻辑在独立的事件循环中运行,从而避免事件循环冲突和关闭错误。
在开发基于FastAPI的异步应用时,我们经常需要对路由进行单元测试。当应用中包含异步操作(例如与MongoDB使用Motor进行交互)时,通常会选择unittest模块提供的IsolatedAsyncioTestCase来编写异步测试用例。然而,一个常见的痛点是遇到RuntimeError: Event loop is closed错误,尤其是在使用TestClient进行多个异步请求时。这个错误表明测试代码尝试在一个已经关闭的或不正确的事件循环上执行异步操作,导致测试失败。
RuntimeError: Event loop is closed错误通常发生在以下几种情况:
解决此问题的关键在于明确划分FastAPI应用代码与测试代码的职责,并遵循unittest.IsolatedAsyncioTestCase的正确使用范式。
将FastAPI应用及其配置(如数据库连接)从测试文件中分离出来,是良好的软件工程实践。这使得应用可以独立运行,测试代码可以独立导入并测试应用的不同部分。
1. 创建 app.py 文件 (FastAPI 应用)
将FastAPI应用的所有路由、模型和应用初始化逻辑放入一个单独的文件,例如 app.py。移除所有与unittest相关的代码。
# app.py
from typing import Optional
import motor.motor_asyncio
import uvicorn
from bson import ObjectId
from fastapi import APIRouter, Body, FastAPI, HTTPException, Request, status
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from pydantic.functional_validators import BeforeValidator
from typing_extensions import Annotated
# -------- Model --------
# 定义PyObjectId类型,用于处理MongoDB的ObjectId
PyObjectId = Annotated[str, BeforeValidator(str)]
class ItemModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
name: str = Field(...)
email: EmailStr = Field(...)
model_config = ConfigDict(
populate_by_name=True,
arbitrary_types_allowed=True,
json_schema_extra={
"example": {"name": "Jane Doe", "email": "jane.doe@example.com"}
},
)
# -------- Router --------
mcve_router = APIRouter()
@mcve_router.post(
"",
response_description="Add new item",
response_model=ItemModel,
status_code=status.HTTP_201_CREATED,
response_model_by_alias=False,
)
async def create_item(request: Request, item: ItemModel = Body(...)):
db_collection = request.app.db_collection
new_item = await db_collection.insert_one(
item.model_dump(by_alias=True, exclude=["id"])
)
created_item = await db_collection.find_one({"_id": new_item.inserted_id})
return created_item
@mcve_router.get(
"/{id}",
response_description="Get a single item",
response_model=ItemModel,
response_model_by_alias=False,
)
async def show_item(request: Request, id: str):
db_collection = request.app.db_collection
if (item := await db_collection.find_one({"_id": ObjectId(id)})) is not None:
return item
raise HTTPException(status_code=404, detail=f"Item {id} not found")
# FastAPI 应用实例
app = FastAPI()
app.include_router(mcve_router, tags=["item"], prefix="/item")
# 数据库客户端和集合配置
app.db_client = motor.motor_asyncio.AsyncIOMotorClient(
"mongodb://127.0.0.1:27017/?readPreference=primary&appname=MongoDB%20Compass&ssl=false"
)
app.db = app.db_client.mcve_db
app.db_collection = app.db.get_collection("bars")
# 应用启动入口 (用于开发和生产环境)
if __name__ == '__main__':
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)在测试文件中,导入FastAPI应用实例,并确保所有涉及异步操作的测试方法都被正确标记为async,并使用await关键字。
2. 创建 test_app.py 文件 (单元测试)
# test_app.py
import asyncio
import unittest
from fastapi.testclient import TestClient
from app import app # 从 app.py 导入 FastAPI 应用实例
class TestAsync(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
"""
在每个测试方法运行前异步设置测试环境。
这里初始化 FastAPI TestClient。
"""
self.client = TestClient(app)
async def asyncTearDown(self):
"""
在每个测试方法运行后异步清理测试环境。
这里关闭 MongoDB 客户端连接。
"""
self.client.app.db_client.close()
# 清理数据库(可选,但推荐在实际项目中进行)
# await self.client.app.db_collection.delete_many({})
async def run_async_test(self, coro):
"""
辅助方法,用于在 IsolatedAsyncioTestCase 的上下文中运行一个协程。
asyncio.run() 会为每次调用创建一个新的事件循环,确保隔离性。
"""
return asyncio.run(coro)
async def test_show_item(self):
"""
测试创建和获取 Item 的异步流程。
整个测试逻辑被封装在一个异步函数中,并通过 run_async_test 执行。
"""
async def test_logic():
bar_data = {"name": "John Doe", "email": "john.doe@example.com"}
# 异步发送 POST 请求创建 Item
create_response = await self.client.post("/item", json=bar_data)
self.assertEqual(create_response.status_code, 201)
created_item_id = create_response.json().get("id")
self.assertIsNotNone(created_item_id)
# 异步发送 GET 请求获取已创建的 Item
response = await self.client.get(f"/item/{created_item_id}")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get("name"), "John Doe")
self.assertEqual(response.json().get("email"), "john.doe@example.com")
self.assertEqual(response.json().get("id"), created_item_id)
# 运行异步测试逻辑
await self.run_async_test(test_logic())
if __name__ == "__main__":
unittest.main()为了运行上述代码,您需要安装以下依赖:
requirements.txt:
fastapi httpx motor pydantic[email] python-bsonjs uvicorn==0.24.0
安装依赖:
pip install -r requirements.txt
MongoDB 设置:
确保您的本地运行着MongoDB实例。如果没有,您可以参考MongoDB官方文档进行安装:MongoDB Installation Guide。
运行测试:
在包含 app.py 和 test_app.py 文件的目录下,执行以下命令来运行单元测试:
python -m unittest test_app.py
如果一切配置正确,您将看到测试成功通过,不再出现“Event loop is closed”错误。
通过上述修改,我们成功解决了在unittest.IsolatedAsyncioTestCase中测试FastAPI异步路由时遇到的“RuntimeError: Event loop is closed”问题。核心思想是:
遵循这些最佳实践,可以有效地构建健壮、可维护的FastAPI异步应用测试套件。
以上就是解决FastAPI异步测试中“Event loop is closed”错误的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号