FastAPI单元测试:如何Mock ServiceNow客户端避免真实API调用
FastAPI单元测试Mock ServiceNow客户端失败问题解决
问题描述
我正在为FastAPI项目编写单元测试,其中一个接口用于获取ServiceNow工单。待测试的接口代码如下:
from aiosnow.models.table.declared import IncidentModel as Incident from fastapi import APIRouter router = APIRouter() @router.post("/get_ticket") async def snow_get_ticket(req: DialogflowRequest): """Retrieves the status of the ticket in the parameter.""" client = create_snow_client( SNOW_TEST_CONFIG.servicenow_url, SNOW_TEST_CONFIG.user, SNOW_TEST_CONFIG.pwd ) params: dict = req.sessionInfo["parameters"] ticket_num = params["ticket_num"] try: async with Incident(client, table_name="incident") as incident: response = await incident.get_one(Incident.number == ticket_num) stage_value = response.data["state"].value desc = response.data["description"] # ... 其他数据处理逻辑,无关部分省略
目前遇到的问题是单元测试无法正确Mock客户端响应,每次都会触发真实的API调用。我写的测试代码如下:
from fastapi.testclient import TestClient client = TestClient(app) @patch("aiosnow.models.table.declared.IncidentModel") def test_get_ticket_endpoint_valid_ticket_num(self, mock_client): mock_client.return_value = {"data" : {"state": "new", "description": "test"}} response = client.post( "/snow/get_ticket", json=json.load(self.test_request) ) assert response.status_code == 200
我怀疑Mock了错误的对象,但不确定应该Mock哪个。
解决方案
你的问题核心在于三个点:Mock路径错误、未正确模拟异步上下文管理器、返回值结构不匹配代码的访问逻辑。以下是具体修正步骤:
1. 修正Mock路径
unittest.mock.patch需要Mock被测试代码实际导入使用的对象路径,而非原模块的路径。假设你的路由代码文件路径是app/routes/snow.py,那么你应该Mockapp.routes.snow.Incident(也就是代码里用as Incident导入的别名对象),而不是原模块的aiosnow.models.table.declared.IncidentModel。
2. 模拟异步上下文管理器
代码中使用了async with Incident(...) as incident:,因此需要模拟Incident类的__aenter__方法,让它返回一个包含get_one方法的Mock对象,才能正确适配异步上下文的逻辑。
3. 匹配返回值结构
代码里访问的是response.data["state"].value,说明state是一个带有value属性的对象,而非直接的字符串。你之前直接返回字典的方式不符合这个结构,需要模拟出对应的层级和属性。
修正后的测试代码示例:
from unittest.mock import patch, AsyncMock from fastapi.testclient import TestClient client = TestClient(app) def test_get_ticket_endpoint_valid_ticket_num(self): # 模拟符合代码访问结构的返回值 mock_state = type('MockState', (), {'value': 'new'})() mock_response = type('MockResponse', (), { 'data': { 'state': mock_state, 'description': 'test' } })() # 模拟Incident的异步上下文和get_one方法 with patch("你的路由文件路径.Incident") as mock_incident_cls: # 让async with返回的incident对象具备get_one方法 mock_incident = AsyncMock() mock_incident.get_one.return_value = mock_response mock_incident_cls.return_value.__aenter__.return_value = mock_incident # 发送测试请求 response = client.post( "/snow/get_ticket", json=json.load(self.test_request) ) # 断言状态码 assert response.status_code == 200 # 可选:断言get_one是否被正确调用 mock_incident.get_one.assert_called_once()
关键注意点
- 将
你的路由文件路径替换为实际路径,例如路由代码在app/api/snow_routes.py,则写app.api.snow_routes.Incident - 使用
AsyncMock处理异步方法,避免触发真实的API请求 - 严格匹配代码中对返回值的访问结构,确保Mock的数据能被正常解析
内容的提问来源于stack exchange,提问作者Showertime
相关产品推荐
相关产品推荐

