【问题标题】:How to mock pydantic BaseModel that expects a Response object?如何模拟需要 Response 对象的 pydantic BaseModel?
【发布时间】:2021-09-03 08:16:28
【问题描述】:

我正在为我的 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)

一种解决方案是添加UnionMagicMock,但我真的不想更改测试代码。这不是办法。

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


    【解决方案1】:

    您可以为测试创建Response 的子类,而不是使用MagicMock/Mock,然后修补requests.get 以返回该子类的实例。

    这让你:

    • 保持你的模拟类型为Response(让pydantic开心)
    • 控制测试的大部分预期响应行为
    • 避免使用测试代码污染应用程序代码(是的,“一种解决方案是添加 UnionMagicMock绝对不是这样。)

    (我将假设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)
    

    【讨论】:

      猜你喜欢
      • 2022-06-14
      • 1970-01-01
      • 2020-01-14
      • 2022-11-22
      • 2020-09-12
      • 2021-12-01
      • 2023-02-04
      • 2021-05-27
      • 2022-10-07
      相关资源
      最近更新 更多