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

Sym*_*nen 4 python unit-testing mocking python-3.x pydantic

我正在为我的 API 客户端编写测试。我需要模拟该get函数,以便它不会发出任何请求。因此,Response我不想返回一个对象,而是想返回一个MagicMock. 但随后 pydantic 加注ValidationError因为它要去模型。

我有以下 pydantic 模型:

class Meta(BaseModel):
    raw: Optional[str]
    response: Optional[Response]

    class Config:
        arbitrary_types_allowed = True
Run Code Online (Sandbox Code Playgroud)

这引发了:

>   ???
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)
Run Code Online (Sandbox Code Playgroud)

一种解决方案是添加UnionwithMagicMock但我真的不想更改测试代码。事实并非如此。

class Meta(BaseModel):
    raw: Optional[str]
    response: Optional[Union[Response, MagicMock]]

    class Config:
        arbitrary_types_allowed = True

Run Code Online (Sandbox Code Playgroud)

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

Gin*_*pin 6

您可以创建for 测试的子类,然后修补,而不是使用MagicMock/MockResponserequests.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))),
            ]
        )
Run Code Online (Sandbox Code Playgroud)

然后,在测试中,您只需要实例化 aMockResponse并告诉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
Run Code Online (Sandbox Code Playgroud)
# 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)
Run Code Online (Sandbox Code Playgroud)