Как я могу издеваться над запросами и ответом?

224

Я пытаюсь использовать пакет Pythons для макета requestsмодуля Pythons . Каковы основные требования, чтобы заставить меня работать в сценарии ниже?

В моем 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:

проверить ответ содержит «ответ», «ответ b», «ответ c»

Как я могу выполнить Шаг 1 (издеваться над модулем запросов)?

kk1957
источник
5
Вот рабочая ссылка cra.mr/2014/05/20/mocking-requests-with-responses
Йогеш Леле

Ответы:

279

Вот как вы можете это сделать (вы можете запустить этот файл как есть):

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, вы должны использовать mock 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 - отличная идея! Я пытался подделать Resest.Response объект класса, но это было нелегко. Я мог бы использовать этот MockResponse вместо реальной вещи. Спасибо!
Йоши
@yoshi Да, мне потребовалось некоторое время, чтобы обернуться вокруг насмешек в Python, но для меня это работает очень хорошо!
Йоханнес Фаренкруг,
10
А в Python 2.x просто замените from unittest import mockна, import mockа остальное работает как есть. Вам нужно установитьmock пакет отдельно.
haridsv
3
Фантастика. Я должен был сделать небольшое изменение в Python 3 по мере mock_requests_getнеобходимости yieldвместо того, чтобы returnиз-за изменения возвращать итераторы в Python 3.
erip
1
это было то, о чем первоначально спрашивал вопрос. Я выяснил способы (упаковать приложение в пакет и закрепить test_client () для выполнения вызова). спасибо за пост, хотя, все еще использовал основную часть кода.
Самоубийственный заяц
143

Попробуйте использовать библиотеку ответов . Вот пример из их документации :

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 .

Anentropic
источник
На первый взгляд, я не нашел способа responsesсопоставить URL-адрес с подстановочными символами, то есть реализовать логику обратного вызова, например, «взять последнюю часть URL-адреса, найти ее на карте и вернуть соответствующее значение». Это возможно, и я просто скучаю по нему?
подонок
1
@scubbo, вы можете передать предварительно скомпилированное регулярное выражение в качестве параметра url и использовать стиль обратного вызова github.com/getsentry/responses#dynamic-responses, что даст вам желаемое поведение с подстановочными знаками (я могу получить доступ к переданному URL по requestаргументу получен
функцией
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)))
kk1957
источник
3
Это будет работать, если вы ожидаете текстовых / html ответов. Если вы издеваетесь над REST API, хотите проверить код состояния и т. Д., То, вероятно, вам поможет ответ от Johannes [ stackoverflow.com/a/28507806/3559967] .
Антоний
5
Для Python 3 используйте from unittest import mock. docs.python.org/3/library/unittest.mock.html
Феникс
33

Я использовал запросы-макет для написания тестов для отдельного модуля:

# 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()
AnaPana
источник
Откуда вы взяли м в '(сам, м):'
Денис Евсеев
16

это то, как вы издеваетесь над examples.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
tingyiy
источник
1
Что делать, если я хочу издеваться над функцией? Как смоделировать это, например: mockresponse.json () = {"key": "value"}
primoz
1
@primoz, я использовал анонимную функцию / лямбду для этого:mockresponse.json = lambda: {'key': 'value'}
Tayler
1
Илиmockresponse.json.return_value = {"key": "value"}
Ларс Блумберг
5

Если вы хотите смоделировать ложный ответ, другой способ сделать это - просто создать экземпляр базового класса HttpResponse, например, так:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()
Том Чапин
источник
Это ответ на то, что я пытался найти: получить поддельный объект ответа django, который может пройти сквозь гамму промежуточного программного обеспечения для почти e2e-теста. HttpResponse, а не ... База, сделал трюк для меня, хотя. Спасибо!
low_ghost
4

Один из возможных способов обойти запросы - использовать библиотеку 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/

Рональд Теодоро
источник
Обратите внимание, что betamax предназначен для работы только с запросами , если вам нужно захватить HTTP-запросы, сделанные пользователем HTTP-API более низкого уровня, например httplib3 , или с альтернативным aiohttp , или клиентскими библиотеками , такими как boto … используйте вместо этого 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(...)а Боб твой дядя .

Макс П Маги
источник
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)

Если у вас есть лучший способ сделать это, скажите мне, но я думаю, что это довольно чисто.

Martbob
источник