0

0

解决FastAPI异步测试中“Event loop is closed”错误

心靈之曲

心靈之曲

发布时间:2025-11-30 13:17:02

|

505人浏览过

|

来源于php中文网

原创

解决fastapi异步测试中“event loop is closed”错误

本文旨在解决在使用unittest.IsolatedAsyncioTestCase测试FastAPI异步路由时遇到的“RuntimeError: Event loop is closed”问题。核心方案是分离FastAPI应用与测试代码,并采用正确的异步测试结构,确保每个异步测试逻辑在独立的事件循环中运行,从而避免事件循环冲突和关闭错误。

引言:FastAPI异步测试中的事件循环关闭问题

在开发基于FastAPI的异步应用时,我们经常需要对路由进行单元测试。当应用中包含异步操作(例如与MongoDB使用Motor进行交互)时,通常会选择unittest模块提供的IsolatedAsyncioTestCase来编写异步测试用例。然而,一个常见的痛点是遇到RuntimeError: Event loop is closed错误,尤其是在使用TestClient进行多个异步请求时。这个错误表明测试代码尝试在一个已经关闭的或不正确的事件循环上执行异步操作,导致测试失败。

问题根源分析

RuntimeError: Event loop is closed错误通常发生在以下几种情况:

  1. 事件循环生命周期管理不当:unittest.IsolatedAsyncioTestCase旨在为每个异步测试方法提供一个独立的、隔离的事件循环。如果在非异步的测试方法中手动获取并运行事件循环,或者在异步方法中对事件循环进行不当操作(如多次关闭),就可能导致后续的异步操作找不到可用的事件循环。
  2. 同步方法中执行异步操作:在原始代码中,test_show_item方法本身是一个同步方法,但它内部调用了self.client.post和self.client.get,这些TestClient的方法在底层会尝试执行异步操作。当一个同步方法尝试在一个由IsolatedAsyncioTestCase管理的异步环境中运行异步代码时,如果没有正确地桥接同步与异步上下文,就容易出现事件循环问题。
  3. TestClient与anyio的交互:FastAPI的TestClient内部依赖anyio库来处理异步请求。anyio会管理其自己的任务和事件循环上下文。当IsolatedAsyncioTestCase和anyio都在尝试管理或使用事件循环时,如果协调不当,便可能导致冲突。原始代码中的run_async_test方法试图通过asyncio.get_event_loop().run_until_complete(coro)来运行协程,这可能获取到错误的事件循环,或者在IsolatedAsyncioTestCase已经关闭其循环后再次尝试使用。

解决方案:优化FastAPI应用与测试结构

解决此问题的关键在于明确划分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()

关键改进点解释:

  1. async def asyncSetUp(self) 和 async def asyncTearDown(self): IsolatedAsyncioTestCase 提供了这些异步的设置和清理方法,确保在每个测试用例运行前后,可以执行异步操作(例如初始化TestClient或关闭数据库连接)。
  2. async def run_async_test(self, coro): 这个辅助方法是关键。它使用asyncio.run(coro)来运行传入的协程。asyncio.run() 的一个重要特性是它会为每次调用创建一个新的事件循环,并在协程完成后关闭它。这确保了每个测试逻辑的执行都在一个干净、隔离的事件循环中,避免了事件循环冲突。
  3. async def test_show_item(self): 测试方法本身被标记为async。这使得它能够在IsolatedAsyncioTestCase的异步上下文中运行。
  4. async def test_logic(): 将实际的测试步骤封装在一个内部的异步函数test_logic中。这样做的好处是,test_logic内部可以直接使用await关键字调用self.client.post和self.client.get等异步方法。
  5. await self.run_async_test(test_logic()): 在test_show_item中,我们await调用self.run_async_test(test_logic())。这意味着test_logic中的所有异步操作都会在一个由asyncio.run创建的独立事件循环中执行,而这个asyncio.run本身又是在IsolatedAsyncioTestCase提供的事件循环中被await的。这种嵌套确保了隔离性和正确的事件循环管理。

环境配置与运行

为了运行上述代码,您需要安装以下依赖:

TalkMe
TalkMe

与AI语伴聊天,练习外语口语

下载

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应用逻辑与测试逻辑完全分离,提高模块化程度。
  • 正确使用异步测试框架:充分利用unittest.IsolatedAsyncioTestCase提供的asyncSetUp、asyncTearDown和async测试方法。
  • 事件循环隔离:通过在测试方法内部使用asyncio.run()来执行核心测试逻辑,确保每个测试用例都在一个独立的事件循环中运行,从而避免了事件循环的冲突和不当关闭。
  • 明确异步调用:在异步测试方法中,所有异步操作(包括TestClient的请求)都应使用await关键字。

遵循这些最佳实践,可以有效地构建健壮、可维护的FastAPI异步应用测试套件。

相关专题

更多
Python FastAPI异步API开发_Python怎么用FastAPI构建异步API
Python FastAPI异步API开发_Python怎么用FastAPI构建异步API

Python FastAPI 异步开发利用 async/await 关键字,通过定义异步视图函数、使用异步数据库库 (如 databases)、异步 HTTP 客户端 (如 httpx),并结合后台任务队列(如 Celery)和异步依赖项,实现高效的 I/O 密集型 API,显著提升吞吐量和响应速度,尤其适用于处理数据库查询、网络请求等耗时操作,无需阻塞主线程。

27

2025.12.22

mongodb和mysql的区别
mongodb和mysql的区别

mongodb和mysql的区别:1、数据模型;2、查询语言;3、扩展性和性能;4、可靠性。本专题为大家提供mongodb和mysql的区别的相关的文章、下载、课程内容,供大家免费下载体验。

281

2023.07.18

mongodb启动命令
mongodb启动命令

MongoDB 是一种开源的、基于文档的 NoSQL 数据库管理系统。本专题提供mongodb启动命令的文章,希望可以帮到大家。

250

2023.08.08

MongoDB删除数据的方法
MongoDB删除数据的方法

MongoDB删除数据的方法有删除集合中的文档、删除整个集合、删除数据库和删除指定字段等。本专题为大家提供MongoDB相关的文章、下载、课程内容,供大家免费下载体验。

160

2023.09.19

常用的数据库软件
常用的数据库软件

常用的数据库软件有MySQL、Oracle、SQL Server、PostgreSQL、MongoDB、Redis、Cassandra、Hadoop、Spark和Amazon DynamoDB。更多关于数据库软件的内容详情请看本专题下面的文章。php中文网欢迎大家前来学习。

972

2023.11.02

mongodb有哪些应用领域
mongodb有哪些应用领域

mongodb 的应用领域涵盖广泛,包括内容管理系统、社交媒体、分析、移动应用、物联网、金融科技、医疗保健和广告技术等领域,因其灵活性、可扩展性和易用性而广受欢迎。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

336

2024.04.02

mongodb和redis哪个读取速度快
mongodb和redis哪个读取速度快

redis 的读取速度比 mongodb 更快。原因包括:1. redis 使用简单的键值存储,而 mongodb 存储 json 格式的数据,需要解析和反序列化。2. redis 使用哈希表快速查找数据,而 mongodb 使用 b-tree 索引。因此,redis 在需要高性能读取操作的应用程序中是一个更好的选择。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

479

2024.04.02

mongodb安装失败如何彻底删除
mongodb安装失败如何彻底删除

彻底删除 mongodb 安装失败的步骤:1、停止和禁用 mongodb 服务;2、删除配置文件、数据目录和日志文件;3、删除 mongodb 二进制文件;4、卸载 mongodb 套件(如果通过软件包管理器安装);5、删除 mongodb 用户、组和目录;6、重启系统。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

372

2024.04.02

PHP WebSocket 实时通信开发
PHP WebSocket 实时通信开发

本专题系统讲解 PHP 在实时通信与长连接场景中的应用实践,涵盖 WebSocket 协议原理、服务端连接管理、消息推送机制、心跳检测、断线重连以及与前端的实时交互实现。通过聊天系统、实时通知等案例,帮助开发者掌握 使用 PHP 构建实时通信与推送服务的完整开发流程,适用于即时消息与高互动性应用场景。

9

2026.01.19

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 4.7万人学习

Django 教程
Django 教程

共28课时 | 3.2万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号