Comment se moquer des demandes et de la réponse?


221

J'essaie d'utiliser le package de simulation Pythons pour simuler le requestsmodule Pythons . Quels sont les appels de base pour me faire travailler dans le scénario ci-dessous?

Dans mon views.py, j'ai une fonction qui fait une variété d'appels request.get () avec une réponse différente à chaque fois

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

Dans ma classe de test, je veux faire quelque chose comme ça, mais je ne peux pas comprendre les appels de méthode exacts

Étape 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'

Étape 2:

Appeler ma vue

Étape 3:

vérifier que la réponse contient «une réponse», «b réponse», «c réponse»

Comment puis-je terminer l'étape 1 (se moquer du module des demandes)?


Réponses:


277

Voici comment vous pouvez le faire (vous pouvez exécuter ce fichier tel quel):

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

Remarque importante: si votre MyGreatClassclasse vit dans un package différent, par exemple my.great.package, vous devez vous moquer my.great.package.requests.getau lieu de simplement 'request.get'. Dans ce cas, votre scénario de test ressemblerait à ceci:

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

Prendre plaisir!


2
La classe MockResponse est une excellente idée! J'essayais de simuler un objet de classe resuests.Response mais ce n'était pas facile. Je pourrais utiliser cette MockResponse à la place de la vraie chose. Je vous remercie!
yoshi

@yoshi Ouais, il m'a fallu du temps pour envelopper ma tête autour de moqueries en Python mais cela fonctionne assez bien pour moi!
Johannes Fahrenkrug

10
Et dans Python 2.x, remplacez simplement from unittest import mockpar import mocket le reste fonctionne tel quel. Vous devez installer le mockpackage séparément.
haridsv

3
Fantastique. J'ai dû faire un léger changement dans Python 3 selon les mock_requests_getbesoins au yieldlieu de à returncause du changement de retour des itérateurs dans Python 3.
erip

1
c'est à cela que la question posait à l'origine. J'ai trouvé des moyens (emballer l'application dans un package et fixer un test_client () pour faire l'appel). merci pour le message, j'utilisais toujours l'épine dorsale du code.
Suicide Bunny

142

Essayez d'utiliser la bibliothèque de réponses . Voici un exemple tiré de leur documentation :

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

Il offre une commodité assez agréable par rapport à la configuration de toutes les moqueries vous-même.

Il y a aussi HTTPretty :

Ce n'est pas spécifique à la requestsbibliothèque, plus puissant à certains égards, bien que je trouve qu'il ne se prête pas si bien à inspecter les requêtes qu'il a interceptées, ce qui se responsesfait assez facilement

Il y a aussi httmock .


En un coup d'œil, je n'ai pas trouvé de moyen de responsesfaire correspondre une URL générique - c'est-à-dire, implémenter une logique de rappel comme "prendre la dernière partie de l'URL, la rechercher dans une carte et renvoyer la valeur correspondante". Est-ce possible et ça me manque juste?
scubbo

1
@scubbo vous pouvez passer une expression régulière pré-compilée en tant que paramètre d'url et utiliser le style de rappel github.com/getsentry/responses#dynamic-responses cela vous donnera le comportement générique que vous voulez, je pense (peut accéder à l'url passée sur l' requestargument reçu par la fonction de rappel)
Anentropic

48

Voici ce qui a fonctionné pour moi:

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
Cela fonctionnera si vous attendez des réponses texte / html. Si vous vous moquez d'une API REST, souhaitez vérifier le code d'état, etc., la réponse de Johannes [ stackoverflow.com/a/28507806/3559967] est probablement la voie à suivre.
Antony

5
Pour Python 3, utilisez from unittest import mock. docs.python.org/3/library/unittest.mock.html
phoenix

32

J'ai utilisé des requêtes-maquette pour écrire des tests pour un module séparé:

# module.py
import requests

class A():

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

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

Où obtenez-vous m dans '(auto, m):'
Denis Evseev

16

c'est ainsi que vous vous moquez de requests.post, changez-le en votre méthode 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
Et si je veux me moquer d'une fonction? Comment se moquer de cela par exemple: mockresponse.json () = {"key": "value"}
primoz

1
@primoz, j'ai utilisé une fonction anonyme / lambda pour cela:mockresponse.json = lambda: {'key': 'value'}
Tayler

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

5

Si vous voulez vous moquer d'une fausse réponse, une autre façon de le faire est d'instancier simplement une instance de la classe HttpResponse de base, comme ceci:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

C'est la réponse à ce que j'essayais de trouver: obtenir un faux objet de réponse django qui peut traverser la gamme de middleware pour un test presque e2e. HttpResponse, plutôt que ... Base, a cependant fait l'affaire pour moi. Merci!
low_ghost

4

Une façon possible de contourner les demandes est d'utiliser la bibliothèque betamax, elle enregistre toutes les demandes et après cela, si vous faites une demande dans la même URL avec les mêmes paramètres, le betamax utilisera la demande enregistrée, je l'ai utilisée pour tester le robot d'indexation Web et ça me fait gagner beaucoup de temps.

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/


Notez que betamax est conçu pour fonctionner uniquement avec les requêtes , si vous avez besoin de capturer des requêtes HTTP faites par des API HTTP utilisateur de niveau inférieur comme httplib3 , ou avec une alternative aiohttp , ou des bibliothèques clientes comme boto ... utilisez plutôt vcrpy qui fonctionne à un niveau inférieur. Plus sur github.com/betamaxpy/betamax/issues/125
Le Hibou

0

Juste un indice utile pour ceux qui sont toujours en difficulté, convertissant d'urllib ou urllib2 / urllib3 en requêtes ET essayant de se moquer d'une réponse - J'obtenais une erreur légèrement déroutante lors de la mise en œuvre de ma simulation:

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

AttributeError: __enter__

Eh bien, bien sûr, si je savais quelque chose sur le withfonctionnement (je ne le savais pas), je saurais que c'était un contexte résiduel et inutile (du PEP 343 ). Inutile lors de l'utilisation de la bibliothèque de requêtes car elle fait essentiellement la même chose pour vous sous le capot . Retirez-le withet utilisez nu requests.get(...)et Bob est votre oncle .


0

J'ajouterai ces informations car j'ai eu du mal à comprendre comment se moquer d'un appel api asynchrone.

Voici ce que j'ai fait pour simuler un appel asynchrone.

Voici la fonction que je voulais tester

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

Vous avez toujours besoin de la classe 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

Vous ajoutez la classe MockResponseAsync

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

    async def getResponse(self):
        return self.response

Voici le test. La chose importante ici est que je crée la réponse avant car la fonction init ne peut pas être asynchrone et l'appel à getResponse est asynchrone donc tout a été extrait.

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

Si vous avez une meilleure façon de le faire, dites-le moi, mais je pense que c'est assez propre comme ça.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.