在同一测试中模拟对同一功能的两个单独响应
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 而不是函数调用的顺序。
也请看看 ,它激发了我的回答。
如果我只调用 requests.get
一次,我的 requests.get
in my class. The
我的 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 而不是函数调用的顺序。
也请看看