本文旨在解决在使用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异步应用测试套件。
# python
# js
# json
# go
# mongodb
# app
# ssl
# ai
# 路由
# 环境配置
# fastapi
# 封装
# 循环
# Event
# 事件
# 异步
# 数据库
# 软件工程
# 都在
# 应用实例
# 如果没有
# 单元测试
# 会为
# 创建一个
# 装在
# 在每个
# 自己的
# 客户端
相关文章:
如何快速搭建高效WAP手机网站吸引移动用户?
企业宣传片制作网站有哪些,传媒公司怎么找企业宣传片项目?
建站之星CMS五站合一模板配置与SEO优化指南
成都品牌网站制作公司,成都营业执照年报网上怎么办理?
如何自定义建站之星网站的导航菜单样式?
网站制作大概要多少钱一个,做一个平台网站大概多少钱?
如何选择网络建站服务器?高效建站必看指南
成都网站制作公司哪家好,四川省职工服务网是做什么用?
c# Task.ConfigureAwait(true) 在什么场景下是必须的
北京网页设计制作网站有哪些,继续教育自动播放怎么设置?
如何在建站之星网店版论坛获取技术支持?
移民网站制作流程,怎么看加拿大移民官网?
建站主机选虚拟主机还是云服务器更好?
外汇网站制作流程,如何在工商银行网站上做外汇买卖?
广州商城建站系统开发成本与周期如何控制?
已有域名能否直接搭建网站?
如何快速生成可下载的建站源码工具?
表情包在线制作网站免费,表情包怎么弄?
如何在Golang中实现微服务服务拆分_Golang微服务拆分与接口管理方法
建站为何优先选择香港服务器?
IOS倒计时设置UIButton标题title的抖动问题
建站之星后台搭建步骤解析:模板选择与产品管理实操指南
文字头像制作网站推荐软件,醒图能自动配文字吗?
如何设计高效校园网站?
官网网站制作腾讯审核要多久,联想路由器newifi官网
制作网站的公司有哪些,做一个公司网站要多少钱?
建站主机数据库如何配置才能提升网站性能?
红河网站制作公司,红河事业单位身份证如何上传?
如何挑选高效建站主机与优质域名?
如何快速登录WAP自助建站平台?
高配服务器限时抢购:企业级配置与回收服务一站式优惠方案
建站之星×万网:智能建站系统+自助建站平台一键生成
网站建设制作、微信公众号,公明人民医院怎么在网上预约?
浅析上传头像示例及其注意事项
如何通过IIS搭建网站并配置访问权限?
婚礼视频制作网站,学习*后期制作的网站有哪些?
建站主机核心功能解析:服务器选择与网站搭建流程指南
制作网页的网站有哪些,电脑上怎么做网页?
相册网站制作软件,图片上的网址怎么复制?
如何通过多用户协作模板快速搭建高效企业网站?
威客平台建站流程解析:高效搭建教程与设计优化方案
如何快速搭建高效香港服务器网站?
如何在腾讯云服务器快速搭建个人网站?
ppt制作免费网站有哪些,ppt模板免费下载网站?
高性价比服务器租赁——企业级配置与24小时运维服务
建站之星安装后界面空白如何解决?
JS中使用new Date(str)创建时间对象不兼容firefox和ie的解决方法(两种)
郑州企业网站制作公司,郑州招聘网站有哪些?
如何选择长沙网站建站模板?H5响应式与品牌定制哪个更优?
php8.4新语法match怎么用_php8.4match表达式替代switch【方法】
*请认真填写需求信息,我们会在24小时内与您取得联系。