如何模拟请求和响应?

399

我正在尝试使用Python的mock包来模拟Python的requests模块。在下面这种情况下,需要哪些基本调用才能让我工作?

在我的views.py中,我有一个函数,每次都使用不同的响应进行各种requests.get()调用。

def myview(request):
  res1 = requests.get('aurl')
  res2 = request.get('burl')
  res3 = request.get('curl')
在我的测试类中,我想做类似于这样的事情,但无法确定正确的方法调用。
步骤1:
# Mock the requests module
# when mockedRequests.get('aurl') is called then return 'a response'
# when mockedRequests.get('burl') is called then return 'b response'
# when mockedRequests.get('curl') is called then return 'c response'

步骤2:

调用我的视图

步骤3:

验证响应是否包含“a response”,“b response”和“c response”

如何完成第1步(模拟请求模块)?


6
这是工作链接:http://cra.mr/2014/05/20/mocking-requests-with-responses。 - Yogesh lele
20个回答

466

这是您可以执行的方式(您可以直接运行此文件):

import requests
import unittest
from unittest import mock

# This is the class we want to test
class MyGreatClass:
    def fetch_json(self, url):
        response = requests.get(url)
        return response.json()

# This method will be used by the mock to replace requests.get
def mocked_requests_get(*args, **kwargs):
    class MockResponse:
        def __init__(self, json_data, status_code):
            self.json_data = json_data
            self.status_code = status_code

        def json(self):
            return self.json_data

    if args[0] == 'http://someurl.com/test.json':
        return MockResponse({"key1": "value1"}, 200)
    elif args[0] == 'http://someotherurl.com/anothertest.json':
        return MockResponse({"key2": "value2"}, 200)

    return MockResponse(None, 404)

# Our test case class
class MyGreatClassTestCase(unittest.TestCase):

    # We patch 'requests.get' with our own method. The mock object is passed in to our test case method.
    @mock.patch('requests.get', side_effect=mocked_requests_get)
    def test_fetch(self, mock_get):
        # Assert requests.get calls
        mgc = MyGreatClass()
        json_data = mgc.fetch_json('http://someurl.com/test.json')
        self.assertEqual(json_data, {"key1": "value1"})
        json_data = mgc.fetch_json('http://someotherurl.com/anothertest.json')
        self.assertEqual(json_data, {"key2": "value2"})
        json_data = mgc.fetch_json('http://nonexistenturl.com/cantfindme.json')
        self.assertIsNone(json_data)

        # We can even assert that our mocked method was called with the right parameters
        self.assertIn(mock.call('http://someurl.com/test.json'), mock_get.call_args_list)
        self.assertIn(mock.call('http://someotherurl.com/anothertest.json'), mock_get.call_args_list)

        self.assertEqual(len(mock_get.call_args_list), 3)

if __name__ == '__main__':
    unittest.main()

重要提示: 如果您的MyGreatClass类位于另一个包中,比如说my.great.package,那么你需要模拟my.great.package.requests.get而不是仅仅使用'request.get'。在这种情况下,您的测试用例应该如下所示:

import unittest
from unittest import mock
from my.great.package import MyGreatClass

# This method will be used by the mock to replace requests.get
def mocked_requests_get(*args, **kwargs):
    # Same as above


class MyGreatClassTestCase(unittest.TestCase):

    # Now we must patch 'my.great.package.requests.get'
    @mock.patch('my.great.package.requests.get', side_effect=mocked_requests_get)
    def test_fetch(self, mock_get):
        # Same as above

if __name__ == '__main__':
    unittest.main()

享受!


2
MockResponse类是一个很好的想法!我曾试图伪造一个requests.Response类对象,但这并不容易。我可以使用这个MockResponse来代替真实的对象。谢谢! - yoshi
10
在 Python 2.x 中,只需使用 import mock 替换 from unittest import mock,其他部分保持不变即可。但您需要单独安装 mock 包。 - haridsv
5
太棒了。我在Python 3中必须进行轻微更改,因为由于Python 3返回迭代器的更改,mock_requests_get需要使用yield而不是return - erip
2
这个解决方案在GET请求中非常有效。我正在尝试将其推广到POST和PUT,但无法理解如何提供额外的数据以供mocked_requests_get内部使用。mocked_requests_get中的所有输入参数都将用于请求。是否有任何方法可以添加更多参数,以便它们不仅用于请求本身,而且仅用于请求之前的数据操作? - user1329187
1
@MontanaBurr 不同意,特别是因为第二部分。在我的情况下这非常重要(而且可能对许多其他人也是如此,因为将测试用例放在与业务类相同的模块中并不常见)。 - Luke
显示剩余12条评论

275
尝试使用响应库。这里有一个来自他们的文档的例子:
import responses
import requests

@responses.activate
def test_simple():
    responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
                  json={'error': 'not found'}, status=404)

    resp = requests.get('http://twitter.com/api/1/foobar')

    assert resp.json() == {"error": "not found"}

    assert len(responses.calls) == 1
    assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
    assert responses.calls[0].response.text == '{"error": "not found"}'

它相当方便,不需要自己设置所有的模拟。

还有HTTPretty... 它不仅适用于requests库,在某些方面更强大,尽管我发现它不太容易检查拦截的请求,而responses库却很容易

还有httmock

最近在备受瞩目的requests库上出现了一个新的库httpx,它为异步操作提供了一流的支持。一个适用于httpx的模拟库是:https://github.com/lundberg/respx


乍一看,我没有看到responses匹配通配符URL的方法 - 也就是说,实现回调逻辑,比如“获取URL的最后一部分,在Map中查找并返回相应的值”。这是可能的吗?还是我漏掉了什么? - scubbo
4
你可以将预编译的正则表达式作为URL参数传递,并使用回调方式https://github.com/getsentry/responses#dynamic-responses。这样做将给你想要的通配符行为(可以在回调函数收到的`request`参数上访问传递的URL)。 - Anentropic
1
如果你使用pytest,请不要使用这个库。 - marti_
1
我经常使用这个库与pytest一起... - Anentropic
4
开发者们注意了,我们所有的单元测试都在pytest上运行,没有任何问题。如果你遇到了任何问题,请在GitHub上提交。 - Beliaev Maksim
显示剩余2条评论

79

这是适合我的方法:

import mock
@mock.patch('requests.get', mock.Mock(side_effect = lambda k:{'aurl': 'a response', 'burl' : 'b response'}.get(k, 'unhandled request %s'%k)))

3
如果你期望得到文本/HTML响应,那么这个方法是可行的。如果你正在模拟REST API、想要检查状态码等,则Johannes的回答可能更适合你。请参考:https://dev59.com/WmUo5IYBdhLWcg3wzSEq#28507806。 - HelloWorld101
12
对于Python 3,请使用from unittest import mock。https://docs.python.org/3/library/unittest.mock.html - phoenix

53

我使用requests-mock编写单独一个模块的测试:

# module.py
import requests

class A():

    def get_response(self, url):
        response = requests.get(url)
        return response.text

还有测试:

# tests.py
import requests_mock
import unittest

from module import A


class TestAPI(unittest.TestCase):

    @requests_mock.mock()
    def test_get_response(self, m):
        a = A()
        m.get('http://aurl.com', text='a response')
        self.assertEqual(a.get_response('http://aurl.com'), 'a response')
        m.get('http://burl.com', text='b response')
        self.assertEqual(a.get_response('http://burl.com'), 'b response')
        m.get('http://curl.com', text='c response')
        self.assertEqual(a.get_response('http://curl.com'), 'c response')

if __name__ == '__main__':
    unittest.main()

在'(self, m):'中,m是从哪里获取的? - Denis Evseev
@DenisEvseev,这是通过注释@requests_mock.mock()传递的。它与此方法非常相似(但更难阅读):@mock.patch('requests.get',side_effect=mocked_requests_get)。def test_fetch(self,mock_get)。 - Max P Magee

36

这是如何模拟requests.post的方法,请将其更改为您的HTTP方法

@patch.object(requests, 'post')
def your_test_method(self, mockpost):
    mockresponse = Mock()
    mockpost.return_value = mockresponse
    mockresponse.text = 'mock return'

    #call your target method now

1
如果我想模拟一个函数怎么办?如何模拟这个例子:mockresponse.json() = {"key": "value"} - primoz
2
@primoz,我用了一个匿名函数/lambda来实现:mockresponse.json = lambda: {'key': 'value'} - salsbury
3
或者 mockresponse.json.return_value = {"key": "value"}。 (说明:该代码行为Python语言中的一行代码,意思是将{"key": "value"}赋值给mockresponse.json.return_value。) - Lars Blumberg

34

这里有一个使用requests响应类的解决方案。在我看来,它更加清晰。

import json
from unittest.mock import patch
from requests.models import Response

def mocked_requests_get(*args, **kwargs):
    response_content = None
    request_url = kwargs.get('url', None)
    if request_url == 'aurl':
        response_content = json.dumps('a response')
    elif request_url == 'burl':
        response_content = json.dumps('b response')
    elif request_url == 'curl':
        response_content = json.dumps('c response')
    response = Response()
    response.status_code = 200
    response._content = str.encode(response_content)
    return response

@mock.patch('requests.get', side_effect=mocked_requests_get)
def test_fetch(self, mock_get):
     response = requests.get(url='aurl')
     assert ...

为了让这对我起作用,我需要将 kwargs.get('url', None) 替换为 args[0] - CodeBiker
1
不要将其更改为args [0],只需在请求中传递URL参数。 requests.get(url =“aurl”) - Rodrigo Loza
2
我真的不喜欢使用 _content,因为它是一个内部方法,但是通过 raw 属性设置内容非常麻烦,所以这是我找到的最好的方法,可以将一个真正的 Response 对象作为修补后的 requests.get 返回值。 - Chris Collett

10

我最初使用了Johannes Farhenkrug这里的回答,对我来说效果很好。我需要模拟requests库,因为我的目标是隔离应用程序并不测试任何第三方资源。

然后我更深入地阅读了Python的Mock库,并意识到我可以用Python的Mock类替换MockResponse类,你可能称之为“测试替身”或“虚假”。

这样做的优点是可以访问像assert_called_withcall_args等功能,无需额外的库。其他优点,如“可读性”或“更符合Python的风格”则是主观的,所以它们可能对您有作用,也可能没有。

这是我的版本,更新为使用Python的Mock而不是测试替身:

import json
import requests
from unittest import mock

# defube stubs
AUTH_TOKEN = '{"prop": "value"}'
LIST_OF_WIDGETS = '{"widgets": ["widget1", "widget2"]}'
PURCHASED_WIDGETS = '{"widgets": ["purchased_widget"]}'


# exception class when an unknown URL is mocked
class MockNotSupported(Exception):
  pass


# factory method that cranks out the Mocks
def mock_requests_factory(response_stub: str, status_code: int = 200):
    return mock.Mock(**{
        'json.return_value': json.loads(response_stub),
        'text.return_value': response_stub,
        'status_code': status_code,
        'ok': status_code == 200
    })


# side effect mock function
def mock_requests_post(*args, **kwargs):
    if args[0].endswith('/api/v1/get_auth_token'):
        return mock_requests_factory(AUTH_TOKEN)
    elif args[0].endswith('/api/v1/get_widgets'):
        return mock_requests_factory(LIST_OF_WIDGETS)
    elif args[0].endswith('/api/v1/purchased_widgets'):
        return mock_requests_factory(PURCHASED_WIDGETS)
    
    raise MockNotSupported


# patch requests.post and run tests
with mock.patch('requests.post') as requests_post_mock:
  requests_post_mock.side_effect = mock_requests_post
  response = requests.post('https://myserver/api/v1/get_widgets')
  assert response.ok is True
  assert response.status_code == 200
  assert 'widgets' in response.json()
  
  # now I can also do this
  requests_post_mock.assert_called_with('https://myserver/api/v1/get_widgets')

Repl.it链接:

https://repl.it/@abkonsta/使用unittestMock进行requestspost#main.py

https://repl.it/@abkonsta/使用test-double进行requestspost#main.py


8
如果您想模拟一个假的响应,另一种方法是简单地实例化基本的HttpResponse类,如下所示:
from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

3
这是我一直在寻找的答案:获取一个虚假的Django响应对象,可以通过中间件的全流程进行近乎端到端的测试。然而,对我来说,使用HttpResponse(而非...Base)就解决了问题。谢谢! - low_ghost

5

虽然我还没有进行太多复杂的测试,但这对我很有效。

import json
from requests import Response

class MockResponse(Response):
    def __init__(self,
                 url='http://example.com',
                 headers={'Content-Type':'text/html; charset=UTF-8'},
                 status_code=200,
                 reason = 'Success',
                 _content = 'Some html goes here',
                 json_ = None,
                 encoding='UTF-8'
                 ):
    self.url = url
    self.headers = headers
    if json_ and headers['Content-Type'] == 'application/json':
        self._content = json.dumps(json_).encode(encoding)
    else:
        self._content = _content.encode(encoding)

    self.status_code = status_code
    self.reason = reason
    self.encoding = encoding

然后,您可以创建响应:

mock_response = MockResponse(
    headers={'Content-Type' :'application/json'},
    status_code=401,
    json_={'success': False},
    reason='Unauthorized'
)
mock_response.raise_for_status()

提供

requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: http://example.com

4

解决requests库的一种方法是使用betamax库,它会记录所有请求,如果你在同样的URL和参数下发起请求,betamax将会使用已记录的请求。我一直在使用它来测试网络爬虫,这节省了我很多时间。

import os

import requests
from betamax import Betamax
from betamax_serializers import pretty_json


WORKERS_DIR = os.path.dirname(os.path.abspath(__file__))
CASSETTES_DIR = os.path.join(WORKERS_DIR, u'resources', u'cassettes')
MATCH_REQUESTS_ON = [u'method', u'uri', u'path', u'query']

Betamax.register_serializer(pretty_json.PrettyJSONSerializer)
with Betamax.configure() as config:
    config.cassette_library_dir = CASSETTES_DIR
    config.default_cassette_options[u'serialize_with'] = u'prettyjson'
    config.default_cassette_options[u'match_requests_on'] = MATCH_REQUESTS_ON
    config.default_cassette_options[u'preserve_exact_body_bytes'] = True


class WorkerCertidaoTRT2:
    session = requests.session()

    def make_request(self, input_json):
        with Betamax(self.session) as vcr:
            vcr.use_cassette(u'google')
            response = session.get('http://www.google.com')

https://betamax.readthedocs.io/en/latest/


1
请注意,betamax 只能与 requests 一起使用,如果您需要捕获通过低级 HTTP API(如 httplib3)或替代品 aiohttp 或客户端库(如 boto)发出的 HTTP 请求,请改用 vcrpy,它可以在更低的级别上工作。更多信息请参见 https://github.com/betamaxpy/betamax/issues/125 - Le Hibou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接