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.