如何模拟需要 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)

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

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 开心)
  • 控制测试的大部分预期响应行为
  • 避免使用测试代码污染应用程序代码(是的,“一个解决方案是添加 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 到 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)