Wie kann ich Anfragen und Antworten verspotten?


221

Ich versuche, das Pythons-Mock-Paket zu verwenden, um das Pythons- requestsModul zu verspotten . Was sind die grundlegenden Anforderungen, um mich im folgenden Szenario zum Arbeiten zu bringen?

In meiner views.py habe ich eine Funktion, die verschiedene Anfragen von request.get () mit jeweils unterschiedlicher Antwort ausführt

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

In meiner Testklasse möchte ich so etwas machen, kann aber keine genauen Methodenaufrufe herausfinden

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

Schritt 2:

Rufen Sie meine Ansicht

Schritt 3:

Antwort überprüfen enthält 'a Antwort', 'b Antwort', 'c Antwort'

Wie kann ich Schritt 1 abschließen (Verspotten des Anforderungsmoduls)?


Antworten:


277

So können Sie es machen (Sie können diese Datei so wie sie ist ausführen):

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

Wichtiger Hinweis: Wenn Ihre MyGreatClassKlasse beispielsweise in einem anderen Paket lebt my.great.package, müssen Sie sich verspotten, my.great.package.requests.getanstatt nur 'request.get'. In diesem Fall würde Ihr Testfall folgendermaßen aussehen:

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

Genießen!


2
Die MockResponse-Klasse ist eine großartige Idee! Ich habe versucht, ein Resuests zu fälschen. Antwortklassenobjekt, aber es war nicht einfach. Ich könnte diese MockResponse anstelle der realen verwenden. Danke dir!
Yoshi

@yoshi Ja, es hat eine Weile gedauert, bis ich mich in Python um Mocks gekümmert habe, aber das funktioniert ziemlich gut für mich!
Johannes Fahrenkrug

10
Und in Python 2.x, ersetzen Sie einfach from unittest import mockmit import mockund der Rest funktioniert wie. Sie müssen das mockPaket separat installieren .
Haridsv

3
Fantastisch. Ich musste nach mock_requests_getBedarf eine geringfügige Änderung in Python 3 vornehmen , yieldanstatt returndie Iteratoren in Python 3 zurückzugeben.
Erip

1
darum ging es in der Frage ursprünglich. Ich habe Wege gefunden (packe die App in ein Paket und richte einen test_client () ein, um den Aufruf auszuführen). danke für den Beitrag, der immer noch das Rückgrat des Codes benutzte.
Suicide Bunny

141

Versuchen Sie es mit der Antwortbibliothek :

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

bietet eine nette Bequemlichkeit gegenüber dem Einrichten aller Verspottungen

Es gibt auch HTTPretty :

Es ist nicht requestsbibliotheksspezifisch, in mancher Hinsicht leistungsfähiger, obwohl ich festgestellt habe, dass es sich nicht so gut dazu eignet, die von ihm abgefangenen Anforderungen zu überprüfen, was responsesrecht einfach ist

Es gibt auch httmock .


Auf einen Blick sah ich keine Möglichkeit responses, eine Platzhalter-URL abzugleichen. Das heißt, Sie implementieren eine Rückruflogik wie "Nehmen Sie den letzten Teil der URL, schlagen Sie ihn in einer Karte nach und geben Sie den entsprechenden Wert zurück". Ist das möglich und ich vermisse es einfach?
Scubbo

1
@scubbo Sie können einen vorkompilierten regulären Ausdruck als URL-Parameter übergeben und den Rückrufstil github.com/getsentry/responses#dynamic-responses verwenden. Dadurch erhalten Sie das gewünschte Platzhalterverhalten (ich kann auf die übergebene URL auf dem requestArgument zugreifen) empfangen von der Rückruffunktion)
Anentropic

48

Folgendes hat bei mir funktioniert:

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
Dies funktioniert, wenn Sie Text- / HTML-Antworten erwarten. Wenn Sie eine REST-API verspotten, den Statuscode usw. überprüfen möchten, ist die Antwort von Johannes [ stackoverflow.com/a/28507806/3559967] wahrscheinlich der richtige Weg.
Antony

5
Verwenden Sie für Python 3 from unittest import mock. docs.python.org/3/library/unittest.mock.html
Phoenix

32

Ich habe Requests-Mock verwendet, um Tests für ein separates Modul zu schreiben:

# module.py
import requests

class A():

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

Und die Tests:

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

Woher bekommst du m in '(self, m):'
Denis Evseev

16

Auf diese Weise verspotten Sie request.post und ändern es in Ihre http-Methode

@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
Was ist, wenn ich eine Funktion verspotten möchte? So verspotten Sie dies zum Beispiel: mockresponse.json () = {"key": "value"}
primoz

1
@primoz, ich habe eine anonyme Funktion / Lambda dafür verwendet:mockresponse.json = lambda: {'key': 'value'}
Tayler

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

5

Wenn Sie eine gefälschte Antwort verspotten möchten, können Sie auch einfach eine Instanz der Basisklasse HttpResponse instanziieren:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

Dies ist die Antwort auf das, was ich zu finden versuchte: Holen Sie sich ein gefälschtes Django-Antwortobjekt, das es für einen fast e2e-Test durch die Bandbreite der Middleware schaffen kann. HttpResponse, anstatt ... Base, hat den Trick für mich gemacht. Vielen Dank!
low_ghost

4

Eine Möglichkeit, Anfragen zu umgehen, ist die Verwendung der Bibliothek betamax. Sie zeichnet alle Anfragen auf. Wenn Sie danach eine Anfrage in derselben URL mit denselben Parametern stellen, verwendet der betamax die aufgezeichnete Anfrage. Ich habe sie zum Testen des Webcrawlers verwendet und es spart mir viel Zeit.

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/


Beachten Sie, dass betamax nur für Anforderungen geeignet ist , wenn Sie HTTP-Anforderungen erfassen müssen, die vom Benutzer auf einer niedrigeren HTTP-API wie httplib3 oder mit alternativem aiohttp oder Client-Bibliotheken wie boto erstellt wurden . Verwenden Sie stattdessen vcrpy, das auf einer niedrigeren Ebene funktioniert. Mehr unter github.com/betamaxpy/betamax/issues/125
Le Hibou

0

Nur ein hilfreicher Hinweis für diejenigen, die immer noch Probleme haben, von urllib oder urllib2 / urllib3 zu Anfragen konvertieren UND versuchen, eine Antwort zu verspotten. Bei der Implementierung meines Versuchs wurde ein etwas verwirrender Fehler angezeigt:

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

AttributeError: __enter__

Wenn ich etwas darüber wüsste, wie es withfunktioniert (ich habe es nicht getan), würde ich natürlich wissen, dass es sich um einen unnötigen Kontext handelt (aus PEP 343 ). Unnötig, wenn Sie die Anforderungsbibliothek verwenden, da sie unter der Haube im Grunde das Gleiche für Sie tut . Entfernen Sie einfach die withund verwenden Sie nackt requests.get(...)und Bob ist Ihr Onkel .


0

Ich werde diese Informationen hinzufügen, da es mir schwer gefallen ist, einen asynchronen API-Aufruf zu verspotten.

Folgendes habe ich getan, um einen asynchronen Anruf zu verspotten.

Hier ist die Funktion, die ich testen wollte

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

Sie benötigen weiterhin die MockResponse-Klasse

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

Sie fügen die MockResponseAsync-Klasse hinzu

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

    async def getResponse(self):
        return self.response

Hier ist der Test. Das Wichtigste hier ist, dass ich die Antwort vorher erstelle, da die Init- Funktion nicht asynchron sein kann und der Aufruf von getResponse asynchron ist, sodass alles ausgecheckt ist.

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

Wenn Sie eine bessere Möglichkeit haben, sagen Sie es mir, aber ich denke, es ist so ziemlich sauber.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.