如何模拟需要 Response 对象的 pydantic BaseModel?
How to mock pydantic BaseModel that expects a Response object?
我正在为我的 API 客户编写测试。我需要模拟 get
函数,这样它就不会发出任何请求。因此,我不想 returning 一个 Response
对象,而是 return 一个 MagicMock
。但是随后 pydantic 加注 ValidationError
因为它要转到模型。
我有以下 pydantic 模型:
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Response]
class Config:
arbitrary_types_allowed = True
其中提出:
> ???
E pydantic.error_wrappers.ValidationError: 1 validation error for OneCallResponse
E meta -> response
E instance of Response expected (type=type_error.arbitrary_type; expected_arbitrary_type=Response)
一种解决方案是添加 Union
和 MagicMock
,但我真的不想更改测试代码。不是这样的。
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Union[Response, MagicMock]]
class Config:
arbitrary_types_allowed = True
有什么想法如何patch/mock吗?
您可以创建 Response
的子类进行测试,而不是使用 MagicMock
/Mock
,然后将 requests.get
修补为 return 一个实例该子类的。
这让您:
- 将模拟的类型保持为
Response
(让 pydantic 开心)
- 控制测试的大部分预期响应行为
- 避免使用测试代码污染应用程序代码(是的,“一个解决方案是添加
Union
和 MagicMock
”是 绝对不是这样。)
(我假设 Response
来自 requests 库。如果不是,则适当调整要模拟的属性和方法. 思路是一样的。)
# TEST CODE
import json
from requests import Response
from requests.models import CaseInsensitiveDict
class MockResponse(Response):
def __init__(self, mock_response_data: dict, status_code: int) -> None:
super().__init__()
# Mock attributes or methods depending on the use-case.
# Here, mock to make .text, .content, and .json work.
self._content = json.dumps(mock_response_data).encode()
self.encoding = "utf-8"
self.status_code = status_code
self.headers = CaseInsensitiveDict(
[
("content-length", str(len(self._content))),
]
)
然后,在测试中,您只需要实例化一个 MockResponse
并告诉 patch
到 return 那:
# APP CODE
import requests
from pydantic import BaseModel
from typing import Optional
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Response]
class Config:
arbitrary_types_allowed = True
def get_meta(url: str) -> Meta:
resp = requests.get(url)
meta = Meta(raw=resp.json()["status"], response=resp)
return meta
# TEST CODE
from unittest.mock import patch
def test_get_meta():
mocked_response_data = {"status": "OK"}
mocked_response = MockResponse(mocked_response_data, 200)
with patch("requests.get", return_value=mocked_response) as mocked_get:
meta = get_meta("http://test/url")
mocked_get.call_count == 1
assert meta.raw == "OK"
assert meta.response == mocked_response
assert isinstance(meta.response, Response)
我正在为我的 API 客户编写测试。我需要模拟 get
函数,这样它就不会发出任何请求。因此,我不想 returning 一个 Response
对象,而是 return 一个 MagicMock
。但是随后 pydantic 加注 ValidationError
因为它要转到模型。
我有以下 pydantic 模型:
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Response]
class Config:
arbitrary_types_allowed = True
其中提出:
> ???
E pydantic.error_wrappers.ValidationError: 1 validation error for OneCallResponse
E meta -> response
E instance of Response expected (type=type_error.arbitrary_type; expected_arbitrary_type=Response)
一种解决方案是添加 Union
和 MagicMock
,但我真的不想更改测试代码。不是这样的。
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Union[Response, MagicMock]]
class Config:
arbitrary_types_allowed = True
有什么想法如何patch/mock吗?
您可以创建 Response
的子类进行测试,而不是使用 MagicMock
/Mock
,然后将 requests.get
修补为 return 一个实例该子类的。
这让您:
- 将模拟的类型保持为
Response
(让 pydantic 开心) - 控制测试的大部分预期响应行为
- 避免使用测试代码污染应用程序代码(是的,“一个解决方案是添加
Union
和MagicMock
”是 绝对不是这样。)
(我假设 Response
来自 requests 库。如果不是,则适当调整要模拟的属性和方法. 思路是一样的。)
# TEST CODE
import json
from requests import Response
from requests.models import CaseInsensitiveDict
class MockResponse(Response):
def __init__(self, mock_response_data: dict, status_code: int) -> None:
super().__init__()
# Mock attributes or methods depending on the use-case.
# Here, mock to make .text, .content, and .json work.
self._content = json.dumps(mock_response_data).encode()
self.encoding = "utf-8"
self.status_code = status_code
self.headers = CaseInsensitiveDict(
[
("content-length", str(len(self._content))),
]
)
然后,在测试中,您只需要实例化一个 MockResponse
并告诉 patch
到 return 那:
# APP CODE
import requests
from pydantic import BaseModel
from typing import Optional
class Meta(BaseModel):
raw: Optional[str]
response: Optional[Response]
class Config:
arbitrary_types_allowed = True
def get_meta(url: str) -> Meta:
resp = requests.get(url)
meta = Meta(raw=resp.json()["status"], response=resp)
return meta
# TEST CODE
from unittest.mock import patch
def test_get_meta():
mocked_response_data = {"status": "OK"}
mocked_response = MockResponse(mocked_response_data, 200)
with patch("requests.get", return_value=mocked_response) as mocked_get:
meta = get_meta("http://test/url")
mocked_get.call_count == 1
assert meta.raw == "OK"
assert meta.response == mocked_response
assert isinstance(meta.response, Response)