Jak mogę kpić z żądań i odpowiedzi?


221

Usiłuję użyć pakietu próbnego Pythons, aby wyśmiewać requestsmoduł Pythons . Jakie są podstawowe wezwania do pracy w poniższym scenariuszu?

W moim views.py mam funkcję, która wykonuje różne wywołania requests.get () z różną odpowiedzią za każdym razem

def myview(request):
  res1 = requests.get('aurl')
  res2 = request.get('burl')
  res3 = request.get('curl')

W mojej klasie testowej chcę zrobić coś takiego, ale nie mogę dokładnie określić wywołań metod

Krok 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'

Krok 2:

Zadzwoń do mojego widoku

Krok 3:

sprawdź odpowiedź zawiera „odpowiedź”, „b odpowiedź”, „c odpowiedź”

Jak mogę wykonać krok 1 (drwiąc z modułu żądań)?


Odpowiedzi:


277

Oto jak możesz to zrobić (możesz uruchomić ten plik tak, jak jest):

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

Ważna uwaga: jeśli twoja MyGreatClassklasa mieszka w innym pakiecie, powiedzmy my.great.package, że musisz kpić my.great.package.requests.getzamiast po prostu „request.get”. W takim przypadku Twój przypadek testowy wyglądałby tak:

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

Cieszyć się!


2
Klasa MockResponse to świetny pomysł! Próbowałem sfałszować obiekt klasy resuests.Response, ale nie było to łatwe. Mógłbym użyć tego MockResponse zamiast prawdziwej rzeczy. Dziękuję Ci!
yoshi

@ yoshi Tak, zajęło mi trochę czasu, aby owinąć głowę kpiną w Pythonie, ale działa to całkiem dobrze dla mnie!
Johannes Fahrenkrug

10
A w Pythonie 2.x, po prostu zastąpić from unittest import mockz import mockreszta działa jak jest. Musisz zainstalować mockpakiet osobno.
haridsv

3
Fantastyczny. Musiałem wprowadzić niewielką zmianę w Pythonie 3 w mock_requests_getrazie potrzeby, yieldzamiast z returnpowodu zmiany w zwracaniu iteratorów w Pythonie 3.
erip

1
o to pierwotnie pytano. Wymyśliłem sposoby (spakuj aplikację do pakietu i ustaw test_client (), aby wykonać wywołanie). dzięki za post, ale nadal używałem szkieletu kodu.
Króliczek samobójców

141

Spróbuj użyć biblioteki odpowiedzi :

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

zapewnia całkiem niezłą wygodę w samodzielnym konfigurowaniu wszystkich kpin

Istnieje również HTTPretty :

Nie jest specyficzny dla requestsbiblioteki, jest bardziej wydajny pod pewnymi względami, chociaż uważam, że nie nadaje się tak dobrze do sprawdzania przechwyconych żądań, co responsesrobi dość łatwo

Istnieje również httmock .


Na pierwszy rzut oka nie znalazłem sposobu na responsesdopasowanie adresu URL z symbolem wieloznacznym - to znaczy zaimplementuj logikę wywołania zwrotnego, na przykład: „weź ostatnią część adresu URL, wyszukaj ją na mapie i zwróć odpowiednią wartość”. Czy to możliwe i po prostu mi tego brakuje?
scubbo

1
@ scubbo możesz przekazać wstępnie skompilowane wyrażenie regularne jako parametr adresu URL i użyć stylu wywołania zwrotnego github.com/getsentry/respactions#dynamic-resp Response, które zapewnią ci zachowanie symboli wieloznacznych, które według mnie (mogę uzyskać dostęp do przekazanego adresu URL na requestarg odebrane przez funkcję wywołania zwrotnego)
Anentropic

48

Oto, co zadziałało dla mnie:

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
Działa to, jeśli oczekujesz odpowiedzi tekstowej / HTML. Jeśli wyśmiewasz interfejs API REST, chcesz sprawdzić kod stanu itp., Prawdopodobnie odpowiedzią jest Johannes [ stackoverflow.com/a/28507806/3559967] .
Antony

5
W przypadku Python 3 użyj from unittest import mock. docs.python.org/3/library/unittest.mock.html
feniks

32

Użyłem próbnego zapytania do napisania testów dla osobnego modułu:

# module.py
import requests

class A():

    def get_response(self, url):
        response = requests.get(url)
        return response.text

A testy:

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

Gdzie dostałeś m in '(self, m):'
Denis Evseev

16

w ten sposób kpisz z requests.post, zmień go na swoją metodę 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
Co jeśli chcę wyśmiewać funkcję? Jak wyśmiewać to na przykład: mockresponse.json () = {"key": "value"}
primoz

1
@primoz, użyłem do tego anonimowej funkcji / lambda:mockresponse.json = lambda: {'key': 'value'}
Tayler

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

5

Jeśli chcesz kpić z fałszywej odpowiedzi, innym sposobem jest po prostu utworzenie instancji podstawowej klasy HttpResponse, na przykład:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

Oto odpowiedź na to, co próbowałem znaleźć: uzyskaj fałszywy obiekt odpowiedzi django, który może przejść przez gamę oprogramowania pośredniego do testu prawie e2e. HttpResponse, zamiast ... Podstawa, jednak załatwiła sprawę. Dzięki!
low_ghost

4

Jednym z możliwych sposobów obejścia żądań jest użycie biblioteki betamax, rejestruje ona wszystkie żądania, a następnie, jeśli złożysz żądanie w tym samym adresie URL z tymi samymi parametrami, betamax użyje zarejestrowanego żądania, użyłem go do przetestowania robota sieciowego i to oszczędza mi dużo czasu.

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/


Zauważ, że betamax jest zaprojektowany tylko do obsługi żądań , jeśli potrzebujesz przechwytywać żądania HTTP wykonane przez interfejs API HTTP niższego poziomu, taki jak httplib3 , lub z alternatywnym aiohttp , lub biblioteki klienta takie jak boto … zamiast tego użyj vcrpy, który działa na niższym poziomie. Więcej na github.com/betamaxpy/betamax/issues/125
Le Hibou,

0

Po prostu pomocna wskazówka dla tych, którzy wciąż się borykają, przechodząc z urllib lub urllib2 / urllib3 do żądań ORAZ próbując wyszydzić odpowiedź - otrzymałem nieco mylący błąd podczas implementacji mojej makiety:

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

AttributeError: __enter__

Oczywiście, gdybym wiedział cokolwiek o tym, jak withdziała (nie wiedziałem), wiedziałbym, że był to szczątkowy, niepotrzebny kontekst (z PEP 343 ). Niepotrzebne przy korzystaniu z biblioteki żądań, ponieważ robi to samo pod maską . Wystarczy usunąć withi użyć goły, requests.get(...)a Bob jest wujem .


0

Dodam te informacje, ponieważ miałem trudności z wymyśleniem, jak wyśmiewać asynchroniczne wywołanie interfejsu API.

Oto, co zrobiłem, aby wyśmiewać połączenie asynchroniczne.

Oto funkcja, którą chciałem przetestować

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

Nadal potrzebujesz klasy 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

Dodaj klasę MockResponseAsync

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

    async def getResponse(self):
        return self.response

Oto test. Ważną rzeczą jest to, że tworzę odpowiedź wcześniej, ponieważ funkcja init nie może być asynchroniczna, a wywołanie getResponse jest asynchroniczne, więc wszystko zostało sprawdzone.

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

Jeśli masz lepszy sposób na zrobienie tego, powiedz mi, ale myślę, że to całkiem czyste.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.