如何模拟请求和响应?


221

我正在尝试使用Pythons模拟包来模拟Pythons requests模块。使我在以下情况下工作的基本要求是什么?

在我的views.py中,我有一个函数,该函数每次都以不同的响应进行各种request.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响应”,“ b响应”,“ c响应”

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


Answers:


277

这是您可以执行的操作(可以按原样运行此文件):

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类是一个好主意!我试图伪造一个resuests.Response类对象,但这并不容易。我可以用这个MockResponse代替真实的东西。谢谢!
yoshi 2015年

@yoshi是的,我花了一些时间将自己的头围绕在Python中的模拟上,但这对我来说很好!
Johannes Fahrenkrug

10
在Python 2.x中,只需将其替换from unittest import mockimport mock,其余的将按原样工作。您确实需要mock单独安装软件包。
haridsv '16

3
太棒了 我不得不做出在Python 3轻微变化mock_requests_get需要yield,而不是return因为换在Python返回迭代3
erip

1
这就是问题的初衷。我已经找到了方法(将应用程序打包到程序包中,并固定一个test_client()进行调用)。尽管感谢您的帖子,但仍在使用代码的主干。
自杀兔子

141

尝试使用响应库

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的最后一部分,在Map中查找并返回相应的值”。有可能吗,我只是想念它?
scubbo

1
@scubbo您可以将预编译的正则表达式作为url参数传递,并使用回调样式github.com/getsentry/responses#dynamic-responses,这将为您提供您想要的通配符行为(可以访问requestarg 上传递的url 由回调函数接收)
Anentropic

48

这对我有用:

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
如果您期望text / html响应,这将起作用。如果您要模拟REST API,要检查状态代码等,那么Johannes [ stackoverflow.com/a/28507806/3559967]的答案可能是正确的方法。
安东尼

5
对于Python 3,请使用from unittest import mockdocs.python.org/3/library/unittest.mock.html
凤凰城

32

我使用请求模拟为单独的模块编写测试:

# 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

16

这是模拟请求的方法,将其更改为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

1
@primoz,我为此使用了一个匿名函数/ lambda:mockresponse.json = lambda: {'key': 'value'}
Tayler

1
mockresponse.json.return_value = {"key": "value"}
Lars Blumberg

5

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

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

这就是我试图找到的答案:获得一个伪造的django响应对象,该对象可以通过整个中间件进行几乎e2e的测试。HttpResponse,而不是... Base帮了我大忙。谢谢!
low_ghost

4

解决请求的一种可能方法是使用库betamax,它记录所有请求,然后,如果您在具有相同参数的相同url中发出请求,则betamax将使用记录的请求,我一直在使用它来测试Web搜寻器它节省了我很多时间。

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/


请注意,如果您需要捕获使用户使用较低级别的HTTP API(如httplib3)或替代aiohttp或客户端库(如boto)发出的HTTP 请求,则betamax仅适用于请求,而应使用vcrpy来使用较低级别的请求。更多信息,请访问github.com/betamaxpy/betamax/issues/125
Le Hibou

0

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

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

AttributeError:__enter__

好吧,当然,如果我对with工作原理一无所知(我不知道),那我就会知道这是一种残余的,不必要的环境(摘自PEP 343)。不必要的使用请求库时,因为它基本上给你同样的事情引擎盖下。只需移开,with然后使用裸露requests.get(...)Bob的叔叔


0

因为我很难弄清楚如何模拟异步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)

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.