如何模拟需要 Response 对象的 pydantic BaseModel?

问题描述 投票:0回答:2

我正在为我的 API 客户端编写测试。我需要模拟

get
函数,以便它不会提出任何请求。因此,我不想返回
Response
对象,而是想返回
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

有什么想法如何修补/模拟它吗?

python python-3.x unit-testing mocking pydantic
2个回答
6
投票

您可以创建

MagicMock
的子类进行测试,然后修补
Mock
以返回该子类的实例,而不是使用
Response
/
requests.get

这可以让您:

  • 将模拟类型保持为
    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
 返回该值:

# 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)
    

0
投票
作为

Gino 的回答,我建议创建一个新类,但子类化 MagicMock

 而不是 
Response
 或 pydantic 模型中提到的任何类。然后重写神奇的
__class__
方法:

class MockRequest(MagicMock): @property def __class__(self): return Response
然后将此对象修补为 

requests.get

 的返回值或您的代码需要的任何内容。
所以:

    您会在测试代码中获得一个
  1. MagicMock
     对象,并受益于其 
    spec
    cing 和其他功能。
  2. Pydantic 获取一个通过
  3. isinstance
     检查的对象。
© www.soinside.com 2019 - 2024. All rights reserved.