Як я можу знущатися над запитами та відповіддю?


222

Я намагаюся використовувати макетний пакет Pythons для знущання над requestsмодулем Pythons . Які основні дзвінки, щоб змусити мене працювати в нижченаведеному сценарії?

В моєму view.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 відповідь", "відповідь"

Як я можу виконати крок 1 (глузуючи з модуля запитів)?


Відповіді:


278

Ось як це можна зробити (ви можете запустити цей файл як є):

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 Так, мені знадобилося деякий час, щоб обернути голову навколо макетів на 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 () для здійснення дзвінка). спасибі за публікацію, хоча все ще використовували основу коду.
Зайчик-самогубство

142

Спробуйте скористатися бібліотекою відповідей . Ось приклад з їх документації :

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

1
@scubbo ви можете передати заздалегідь складений регулярний вираз як парам-адреса URL-адреси та використовувати стиль зворотного виклику github.com/getsentry/responses#dynamic-respoances, це дасть вам поведінку підстановки, яку ви хочете, я думаю (можна отримати доступ до переданого URL-адреси на requestarg отриманий функцією зворотного дзвінка)
Анентроп

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
Це спрацює, якщо ви очікуєте відповіді на текст / html. Якщо ви знущаєтесь з API REST, хочете перевірити код статусу тощо. Тоді відповідь від Йоганнеса [ 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()

Звідки ти берешся в м ’(я, м): '
Денис Євсеєв

16

ось так ви знущаєтеся з request.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

1
@primoz, я використав для цього анонімну функцію / лямбда:mockresponse.json = lambda: {'key': 'value'}
Тейлер

1
Абоmockresponse.json.return_value = {"key": "value"}
Ларс Блюмберг

5

Якщо ви хочете знущатися з фальшивої відповіді, інший спосіб зробити це - просто інстанціювати екземпляр базового класу HttpResponse, наприклад:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

Це відповідь на те, що я намагався знайти: отримати підроблений об’єкт відповіді на джанго, який зможе зробити це через гаму проміжного програмного забезпечення для майже тесту 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 або клієнтом LIBS як Бото ... використання vcrpy замість який працює на більш низькому рівні. Більше на github.com/betamaxpy/betamax/isissue/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)

Якщо у вас є кращий спосіб зробити це, скажіть, але я думаю, що це досить чисто.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.