在同一测试中模拟对同一功能的两个单独响应

Mock two separate responses to same function in same test

如果我只调用 requests.get 一次,我的 question, I asked how to Mock a class that wraps requests.get in my class. The 提供的效果很好。然而,事实证明我的 class 比我做的例子更复杂。

我的 class 调用了 request.get 两次。一旦初始化,因为它达到了 returns API 值的 API 终点,我需要在我的实际请求中使用,并且一旦我进行 .fetch 调用。

import requests
class ExampleAPI(object):
    def __init__(self):
        self.important_tokens = requests.get(url_to_tokens)['tokens']

    def fetch(self, url, params=None, key=None, token=None, **kwargs):
        return requests.get(url, params=self.important_tokens).json() 

现在,我需要创建两个模拟响应。一个用于初始化,一个用于 .fetch。使用上一个答案中的代码:

@patch('mymodule.requests.get')
def test_fetch(self, fake_get):
    expected = {"result": "True"}
    fake_get.return_value.json.return_value = expected
    e = ExampleAPI()    # This needs one set of mocked responses
    self.assertEqual(e.fetch('http://my.api.url.example.com'), expected)    # This needs a second set

如何为对 request.get 的这两个单独调用创建单独的响应?

您可以将可迭代对象分配给模拟对象的 side_effects 属性;每次调用模拟时,它 returns 可迭代对象的下一个项目。

fake_responses = [Mock(), Mock()]
fake_responses[0].json.return_value = ...
fake_responses[1].json.return_value = ...
fake_get.side_effects = fake_responses

看起来之前的答案使用的是 "side_effects" 而不是 "side_effect"。在 Python 3:

中,您可以这样做
import requests
import unittest
from unittest import mock
from unittest.mock import Mock


class Tests(unittest.TestCase):

    @mock.patch('requests.get')
    def test_post_price_band(self, fake_get):
        fake_responses = [Mock(), Mock()]
        fake_responses[0].json.return_value = {"a": 1}
        fake_responses[1].json.return_value = {"b": 2}
        fake_get.side_effect = fake_responses

        r1 = requests.get('https://www.api.com').json()
        self.assertEqual(r1, {"a": 1})

        r2 = requests.get('https://www.api.com').json()
        self.assertEqual(r2, {"b": 2})

或者你可以这样实现它:

class MockResponse:
    def __init__(self, json_data, status_code=requests.codes.ok):
        self.json_data = json_data
        self.status_code = status_code

    def json(self):
        return self.json_data


class Tests(unittest.TestCase):

    @mock.patch('requests.get')
    def test_post_price_band(self, fake_get):
        fake_get.side_effect = [
            MockResponse({"a": 1}),
            MockResponse({"b": 2})
        ]

        r1 = requests.get('https://www.api.com')
        self.assertEqual(r1.status_code, requests.codes.ok)
        self.assertEqual(r1.json(), {"a": 1})

        r2 = requests.get('https://www.api.com')
        self.assertEqual(r2.status_code, requests.codes.ok)
        self.assertEqual(r2.json(), {"b": 2})

另请查看此库以帮助您:https://github.com/getsentry/responses

我想对给定答案的改进可能是将对象传递给 side_effect 参数。在可迭代的情况下,假设我们正在模拟 requests.get 被调用 n 次,为了模拟每个 request.get 调用,我们传递一个长度为 n 的可迭代。

对可迭代对象的可能改进可能是一个对象,该对象 return 是一个基于模拟函数应该使用的参数之一的模拟函数。在 requests.get 的情况下,它可以是 url。下面的class演示了这种用法。

class UrlSideEffect:
    def __init__(self, url_fn_map: dict):
        """this class returns a function according to url being passed in the mock call"""
        self.url_fn_map = url_fn_map
    
    def __call__(self, *args, **kwargs):
        current_url = kwargs["url"]
        f = self.url_fn_map.get(current_url)
        return f(*args, **kwargs)

Mock 响应 class 其行为类似于模拟对象,模拟函数将 return:

from requests import Response
from requests.structures import CaseInsensitiveDict

class MockResponse(Response):
    def __init__(self, status_code: int, headers: Dict, text: str) -> None:
        super().__init__()
        self.status_code = status_code
        self.headers = headers
        self._text = text

    text = property(lambda obj: obj._text)

把所有东西放在一起......

mocked_response_url1 = MockResponse(200, {headers:1}, "{json_response:ok, url:url1}")
mocked_response_url2 = MockResponse(200, {headers:1}, "{json_response:ok, url:url2}")

side_effect_mocked_response = UrlSideEffect(
    url_fn_map={"https://url1/": lambda *args, **kwargs: mocked_response_url1,
                "https://url2/":lambda *args, **kwargs: mocked_response_url2
               }
          )

import unittest
class TestExampleAPI(unittest.TestCase):
    @mock.patch("requests.get", side_effect=side_effect_mocked_response)
    def test_initialization_and_fetch(self, _: Any) -> None:
        api = ExampleAPI() # requests.get will return mocked_response_url1
        resp = api.fetch(url2) #requests.get will return mocked_response_url2
        
        # here goes whatever you want to assert ... 

这种方法也可以用于异步请求的情况。想象一下 asyncio.gather 正在执行的任务列表。由于请求的顺序不是同步的,我们无法将可迭代传递给 side_effect 参数。因此在这种情况下 class UrlSideEffect 可能很有用,因为它绑定到 url 而不是函数调用的顺序。

也请看看 ,它激发了我的回答。