如何模拟请求和响应?

我试图使用python模拟包来模拟python的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:

调用我的视图

步骤3:

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

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

390334 次浏览

以下是对我有效的方法:

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

尝试使用反应图书馆。下面是一个来自他们的文档的例子:

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"}'

相比于自己设置所有的mock,它提供了相当好的便利。

还有HTTPretty:

它并不特定于requests库,在某些方面更强大,尽管我发现它不太适合检查它拦截的请求,而responses很容易做到这一点

还有httmock

最近一个比古老的requests更受欢迎的新库是httpx,它增加了对async的一等支持。httpx的模拟库是:https://github.com/lundberg/respx

你可以这样做(你可以按原样运行这个文件):

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

享受吧!

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

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

from django.http.response import HttpResponseBase


self.fake_response = HttpResponseBase()

这就是模拟请求的方法。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

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

< a href = " https://betamax.readthedocs。Io /en/latest/" rel="nofollow noreferrer">https://betamax.readthedocs.io/en/latest/

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

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

AttributeError: __enter__

当然,如果我知道with是如何工作的(我不知道),我就会知道它是一个残留的、不必要的上下文(来自PEP 343)。当使用请求库时不需要,因为它基本上为你在引擎盖下做同样的事情。只需删除with并使用裸露的requests.get(...)鲍勃是你的叔叔

我将添加这些信息,因为我很难弄清楚如何模拟异步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

下面是测试。这里重要的是我之前创建了响应,因为初始化函数不能是异步的,对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)

如果你有更好的方法告诉我,不过我觉得这样很干净。

我从约翰内斯Farhenkrug的答案在这里开始,它对我来说很有用。我需要模拟请求库,因为我的目标是隔离我的应用程序,不测试任何第三方资源。

然后我读了一些关于python的模拟库的更多内容,我意识到我可以用python Mock类替换MockResponse类,你可以称之为“Test Double”或“Fake”。

这样做的好处是可以访问assert_called_withcall_args等。不需要额外的库。额外的好处,如“可读性”或“更python化”是主观的,所以它们可能对你有影响,也可能没有。

这是我的版本,更新使用python的Mock而不是test double:

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。它的链接:

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

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

下面是一个带有请求响应类的解决方案。恕我直言,它更干净。

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 ...

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

例如,模拟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'


这对我来说是可行的,尽管我还没有做太多复杂的测试。

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

目前最简单的方法:

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

你可以用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

你也可以在Pytest之外的框架中使用requests_mock——文档很棒。

对于那些不想为pytest安装额外库的人,在那里就是一个例子。我将在这里复制一些扩展,基于上面的例子:

import datetime


import requests




class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
self.elapsed = datetime.timedelta(seconds=1)


# mock json() method always returns a specific testing dictionary
def json(self):
return self.json_data




def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse({'mock_key': 'mock_value'}, 418)


# apply the monkeypatch for requests.get to mock_get
monkeypatch.setattr(requests, 'get', mock_get)


# app.get_json, which contains requests.get, uses the monkeypatch
response = requests.get('https://fakeurl')
response_json = response.json()


assert response_json['mock_key'] == 'mock_value'
assert response.status_code == 418
assert response.elapsed.total_seconds() == 1




============================= test session starts ==============================
collecting ... collected 1 item


test_so.py::test_get_json PASSED                                          [100%]


============================== 1 passed in 0.07s ===============================

我将演示如何通过将真正的请求与返回相同数据的假请求交换来将编程逻辑与实际的外部库分离。在你看来,如果外部api调用,那么这个过程是最好的

import pytest
from unittest.mock import patch
from django.test import RequestFactory


@patch("path(projectname.appname.filename).requests.post")
def test_mock_response(self, mock_get, rf: RequestFactory):
mock_get.return_value.ok = Mock(ok=True)
mock_get.return_value.status_code = 400
mock_get.return_value.json.return_value = {you can define here dummy response}
request = rf.post("test/", data=self.payload)
response = view_name_view(request)


expected_response = {
"success": False,
"status": "unsuccessful",
}


assert response.data == expected_response
assert response.status_code == 400

使用requests_mock很容易修补任何请求

pip install requests-mock
from unittest import TestCase
import requests_mock
from <yourmodule> import <method> (auth)


class TestApi(TestCase):
@requests_mock.Mocker()
def test_01_authentication(self, m):
"""Successful authentication using username password"""
token = 'token'
m.post(f'http://localhost/auth', json= {'token': token})
act_token =auth("user", "pass")
self.assertEqual(act_token, token)


对于避免安装其他依赖项,您应该创建一个假响应。这个FakeResponse可以是Response的子类(我认为这是一个很好的方法,因为它更现实),或者只是一个具有您需要的属性的简单类。

简单的假类

class FakeResponse:
status_code = None


def __init__(self, *args, **kwargs):
self.status_code = 500
self.text = ""

回应之子

class FakeResponse(Response):
encoding = False
_content = None


def __init__(*args, **kwargs):
super(FakeResponse).__thisclass__.status_code = 500
# Requests requires to be not be None, if not throws an exception
# For reference: https://github.com/psf/requests/issues/3698#issuecomment-261115119
super(FakeResponse).__thisclass__.raw = io.BytesIO()

如果使用pytest:

>>> import pytest
>>> import requests


>>> def test_url(requests_mock):
...     requests_mock.get('http://test.com', text='data')
...     assert 'data' == requests.get('http://test.com').text

取自官方文档