【问题标题】:How can I mock requests and the response?如何模拟请求和响应?
【发布时间】:2013-03-23 02:44:57
【问题描述】:

我正在尝试使用 Pythons mock package 来模拟 Pythons 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 步:

调用我的观点

第三步:

验证响应包含'a response'、'b response'、'c response'

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

【问题讨论】:

标签: python mocking request


【解决方案1】:

你能改用requests-mock吗?

假设你的 myview 函数取而代之的是一个 requests.Session 对象,用它发出请求,并对输出做一些事情:

# mypackage.py
def myview(session):
    res1 = session.get("http://aurl")
    res2 = session.get("http://burl")
    res3 = session.get("http://curl")
    return f"{res1.text}, {res2.text}, {res3.text}"
# test_myview.py
from mypackage import myview
import requests

def test_myview(requests_mock):
    # set up requests
    a_req = requests_mock.get("http://aurl", text="a response")
    b_req = requests_mock.get("http://burl", text="b response")
    c_req = requests_mock.get("http://curl", text="c response")

    # test myview behaviour
    session = requests.Session()
    assert myview(session) == "a response, b response, c response"

    # check that requests weren't called repeatedly
    assert a_req.called_once
    assert b_req.called_once
    assert c_req.called_once
    assert requests_mock.call_count == 3

您还可以将requests_mock 与 Pytest 以外的框架一起使用 - 文档很棒。

【讨论】:

    【解决方案2】:

    目前为止最简单的方法:

    from unittest import TestCase
    from unittest.mock import Mock, patch
    
    from .utils import method_foo
    
    
    class TestFoo(TestCase):
    
        @patch.object(utils_requests, "post")  # change to desired method here
        def test_foo(self, mock_requests_post):
            # EXPLANATION: mocked 'post' method above will return some built-in mock, 
            # and its method 'json' will return mock 'mock_data',
            # which got argument 'return_value' with our data to be returned
            mock_data = Mock(return_value=[{"id": 1}, {"id": 2}])
            mock_requests_post.return_value.json = mock_data
    
            method_foo()
    
            # TODO: asserts here
    
    
    """
    Example of method that you can test in utils.py
    """
    def method_foo():
        response = requests.post("http://example.com")
        records = response.json()
        for record in records:
            print(record.get("id"))
            # do other stuff here
    

    【讨论】:

      【解决方案3】:

      这是一个带有 requests Response 类的解决方案。恕我直言,它更干净。

      import json
      from unittest.mock import patch
      from requests.models import Response
      
      def mocked_request_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]
      • 不要将其更改为 args[0],只需在请求中传递 URL 参数即可。 requests.get(url="aurl")
      • 我真的不喜欢使用_content,因为它是一种内部方法,但是尝试通过raw 属性设置内容非常麻烦,所以这是我发现的最好的方法得到一个真正的 Response 对象作为修补后的 requests.get 返回值。
      【解决方案4】:

      这对我有用,虽然我还没有做太多复杂的测试。

      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
      

      【讨论】:

        【解决方案5】:

        对于 pytest 用户,https://pypi.org/project/pytest-responsemock/ 提供了一个方便的夹具

        例如,将 GET 模拟为 http://some.domain,您可以:

        def test_me(response_mock):
        
            with response_mock('GET http://some.domain -> 200 :Nice'):
                response = send_request()
                assert result.ok
                assert result.content == b'Nice'
        
        

        【讨论】:

          【解决方案6】:

          我从Johannes Farhenkrug 的答案here 开始,它对我很有用。我需要模拟 requests 库,因为我的目标是隔离我的应用程序,而不是测试任何第三方资源。

          然后我阅读了有关 python 的 Mock 库的更多信息,我意识到我可以用 python Mock 类替换 MockResponse 类,你可能称之为“Test Double”或“Fake”。

          这样做的好处是可以访问assert_called_withcall_args 等内容。不需要额外的库。诸如“可读性”或“它更 Pythonic”之类的其他好处是主观的,因此它们可能对您起作用,也可能不会起作用。

          这是我的版本,使用 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/Using-unittestMock-for-requestspost#main.py

          https://repl.it/@abkonsta/Using-test-double-for-requestspost#main.py

          【讨论】:

            【解决方案7】:

            尝试使用responses library。以下是来自their documentation 的示例:

            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

            【讨论】:

            • 一目了然,我没有看到responses 匹配通配符 url 的方法——即实现回调逻辑,如“取 url 的最后一部分,在映射,并返回相应的值”。这可能吗,我只是想念它吗?
            • @scubbo 您可以将预编译的正则表达式作为 url 参数传递并使用回调样式 github.com/getsentry/responses#dynamic-responses 这将为您提供我认为您想要的通配符行为(可以访问 @ 上传递的 url 987654330@ arg 被回调函数接收)
            【解决方案8】:

            我将添加此信息,因为我很难弄清楚如何模拟异步 api 调用。

            这是我模拟异步调用所做的。

            这是我要测试的功能

            async def get_user_info(headers, payload):
                return await httpx.AsyncClient().post(URI, json=payload, headers=headers)
            

            你仍然需要 MockResponse 类

            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
            

            您添加 MockResponseAsync 类

            class MockResponseAsync:
                def __init__(self, json_data, status_code):
                    self.response = MockResponse(json_data, status_code)
            
                async def getResponse(self):
                    return self.response
            

            这是测试。这里重要的是我之前创建了响应,因为 init 函数不能是异步的,并且对 getResponse 的调用是异步的,所以它都被检查出来了。

            @pytest.mark.asyncio
            @patch('httpx.AsyncClient')
            async def test_get_user_info_valid(self, mock_post):
                """test_get_user_info_valid"""
                # Given
                token_bd = "abc"
                username = "bob"
                payload = {
                    'USERNAME': username,
                    'DBNAME': 'TEST'
                }
                headers = {
                    'Authorization': 'Bearer ' + token_bd,
                    'Content-Type': 'application/json'
                }
                async_response = MockResponseAsync("", 200)
                mock_post.return_value.post.return_value = async_response.getResponse()
            
                # When
                await api_bd.get_user_info(headers, payload)
            
                # Then
                mock_post.return_value.post.assert_called_once_with(
                    URI, json=payload, headers=headers)
            

            如果你有更好的方法,请告诉我,但我认为这样很干净。

            【讨论】:

              【解决方案9】:

              只是对那些仍在苦苦挣扎的人的有用提示,从 urllib 或 urllib2/urllib3 转换为请求并尝试模拟响应 - 我在实现模拟时遇到了一个稍微令人困惑的错误:

              with requests.get(path, auth=HTTPBasicAuth('user', 'pass'), verify=False) as url:

              属性错误:__enter__

              当然,如果我知道with 的工作原理(我不知道),我会知道这是一个退化的、不必要的context(来自PEP 343)。使用 requests 库时是不必要的,因为它对您基本相同 under the hood。只需删除with 并使用裸requests.get(...)Bob's your uncle

              【讨论】:

                【解决方案10】:

                解决请求的一种可能方法是使用库 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/

                【讨论】:

                【解决方案11】:

                您可以这样做(您可以按原样运行此文件):

                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()
                

                享受吧!

                【讨论】:

                • MockResponse 类是个好主意!我试图伪造一个 resuets.Response 类对象,但这并不容易。我可以用这个 MockResponse 代替真实的东西。谢谢!
                • 在 Python 2.x 中,只需将 from unittest import mock 替换为 import mock,其余部分照常工作。您确实需要单独安装mock 包。
                • 太棒了。由于 Python 3 中返回迭代器的更改,我不得不在 Python 3 中稍作更改,因为 mock_requests_get 需要 yield 而不是 return
                • 此解决方案适用于 GET 请求。我正在尝试将其概括为 POST 和 PUT,但无法理解如何提供额外的数据以在 mocked_requests_get 中使用。 mocked_requests_get 中的所有输入参数都将在请求中使用。有没有办法添加更多的参数,使得它们不用于请求本身,而仅用于请求之前的数据操作?
                • 这就是问题最初所要问的。我已经找到了方法(将应用程序打包并固定一个 test_client() 来进行调用)。感谢您的帖子,但仍在使用代码的主干。
                【解决方案12】:

                这就是你模拟 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
                

                【讨论】:

                • 如果我想模拟一个函数怎么办?例如如何模拟这个: mockresponse.json() = {"key": "value"}
                • @primoz,我为此使用了一个匿名函数/lambda:mockresponse.json = lambda: {'key': 'value'}
                • mockresponse.json.return_value = {"key": "value"}
                【解决方案13】:

                这对我有用:

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

                【讨论】:

                【解决方案14】:

                如果你想模拟一个假响应,另一种方法是简单地实例化一个基础 HttpResponse 类的实例,如下所示:

                from django.http.response import HttpResponseBase
                
                self.fake_response = HttpResponseBase()
                

                【讨论】:

                • 这就是我试图找到的答案:获取一个假的 django 响应对象,它可以通过中间件的范围进行几乎 e2e 测试。 HttpResponse,而不是 ...Base,虽然对我有用。谢谢!
                【解决方案15】:

                我使用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()
                

                【讨论】:

                • 你从哪里得到 m in '(self, m):'
                • @DenisEvseev,这是通过注解 @requests_mock.mock() 传入的。它与这种方法非常相似(但更难阅读):@mock.patch('requests.get', side_effect=mocked_requests_get)。 def test_fetch(self, mock_get):
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2017-04-15
                • 2020-04-25
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多