¿Cómo puedo burlarme de las solicitudes y la respuesta?


221

Estoy tratando de usar el paquete simulado Pythons para simular el requestsmódulo Pythons . ¿Cuáles son las llamadas básicas para que trabaje en el siguiente escenario?

En mi views.py, tengo una función que hace una variedad de llamadas request.get () con diferentes respuestas cada vez

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

En mi clase de prueba quiero hacer algo como esto, pero no puedo entender las llamadas a métodos exactos

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

Paso 2:

Llama mi vista

Paso 3:

verificar respuesta contiene 'una respuesta', 'b respuesta', 'c respuesta'

¿Cómo puedo completar el Paso 1 (burlándose del módulo de solicitudes)?


Respuestas:


277

Así es como puede hacerlo (puede ejecutar este archivo tal cual):

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

Nota importante: si su MyGreatClassclase vive en un paquete diferente, digamos my.great.package, debe burlarse en my.great.package.requests.getlugar de simplemente 'request.get'. En ese caso, su caso de prueba se vería así:

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

¡Disfrutar!


2
¡La clase MockResponse es una gran idea! Estaba tratando de falsificar un objeto de clase Respuestas, pero no fue fácil. Podría usar este MockResponse en lugar de lo real. ¡Gracias!
yoshi

@yoshi Sí, me tomó un tiempo entender mis simulacros en Python, ¡pero esto funciona bastante bien para mí!
Johannes Fahrenkrug

10
Y en Python 2.x, simplemente reemplace from unittest import mockcon import mocky el resto funciona como está. Necesita instalar el mockpaquete por separado.
haridsv

3
Fantástico. Tuve que hacer un ligero cambio en Python 3 según fuera mock_requests_getnecesario en yieldlugar de returndebido al cambio en los iteradores que regresan en Python 3.
erip

1
de eso se trataba originalmente la pregunta. He descubierto formas (empacar la aplicación en un paquete y fijar un test_client () para hacer la llamada). Sin embargo, gracias por la publicación, todavía estaba usando la columna vertebral del código.
Suicide Bunny

142

Intenta usar la biblioteca de respuestas . Aquí hay un ejemplo de su documentación :

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

Proporciona una comodidad bastante agradable sobre la configuración de todas las burlas usted mismo.

También hay HTTPretty :

No es específico de la requestsbiblioteca, de alguna manera es más potente, aunque descubrí que no se presta muy bien para inspeccionar las solicitudes que interceptó, lo que responseses bastante fácil.

También hay httmock .


De un vistazo, no vi una manera de responseshacer coincidir una url comodín, es decir, implementar una lógica de devolución de llamada como "tomar la última parte de la url, buscarla en un mapa y devolver el valor correspondiente". ¿Es eso posible y me lo estoy perdiendo?
scubbo

1
@scubbo, puede pasar una expresión regular precompilada como parámetro de url y usar el estilo de devolución de llamada github.com/getsentry/responses#dynamic-responses esto le dará el comportamiento comodín que desea, creo (puede acceder a la url pasada en el requestargumento recibido por la función de devolución de llamada)
Anentropic

48

Esto es lo que funcionó para mí:

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
Esto funcionará si espera respuestas de texto / html. Si se está burlando de una API REST, desea verificar el código de estado, etc., entonces la respuesta de Johannes [ stackoverflow.com/a/28507806/3559967] es probablemente el camino a seguir.
Antony

55
Para Python 3, use from unittest import mock. docs.python.org/3/library/unittest.mock.html
phoenix

32

Usé peticiones simuladas para escribir pruebas para un módulo separado:

# module.py
import requests

class A():

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

Y las pruebas:

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

¿Dónde obtienes m en '(self, m):'
Denis Evseev

16

así es como se burlan de request.post, cámbielo a su método 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
¿Qué pasa si quiero burlarme de una función? Cómo burlarse de esto, por ejemplo: mockresponse.json () = {"key": "value"}
primoz

1
@primoz, usé una función / lambda anónima para eso:mockresponse.json = lambda: {'key': 'value'}
Tayler

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

5

Si desea burlarse de una respuesta falsa, otra forma de hacerlo es simplemente instanciar una instancia de la clase base HttpResponse, de esta manera:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

Esta es la respuesta a lo que estaba tratando de encontrar: obtener un objeto de respuesta falso de django que pueda atravesar la gama de middleware para una prueba casi e2e. HttpResponse, en lugar de ... Base, hizo el truco para mí. ¡Gracias!
low_ghost

4

Una posible forma de evitar las solicitudes es usar la biblioteca betamax, registra todas las solicitudes y luego, si realiza una solicitud en la misma URL con los mismos parámetros, betamax usará la solicitud registrada, la he estado usando para probar el rastreador web y me ahorró mucho tiempo

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/


Tenga en cuenta que betamax está diseñado para funcionar solo con solicitudes , si necesita capturar solicitudes HTTP hechas por el usuario de nivel inferior API HTTP como httplib3 , o con aiohttp alternativo , o bibliotecas de cliente como boto ... use vcrpy en su lugar, que funciona en el nivel inferior. Más en github.com/betamaxpy/betamax/issues/125
Le Hibou

0

Solo una sugerencia útil para aquellos que todavía están luchando, convirtiendo de urllib o urllib2 / urllib3 a solicitudes Y tratando de burlarse de una respuesta: recibí un error un poco confuso al implementar mi simulación:

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

AttributeError: __enter__

Bueno, por supuesto, si supiera algo sobre cómo withfunciona (no lo sé), sabría que es un contexto vestigial e innecesario (de PEP 343 ). Innecesario cuando se utiliza la biblioteca de solicitudes, ya que básicamente hace lo mismo para usted bajo el capó . Solo quítatelo withy usa desnudo requests.get(...)y Bob es tu tío .


0

Agregaré esta información ya que me costó mucho imaginarme cómo burlarme de una llamada API asíncrona.

Esto es lo que hice para burlarme de una llamada asincrónica.

Aquí está la función que quería probar

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

Aún necesitas la clase 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

Agrega la clase MockResponseAsync

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

    async def getResponse(self):
        return self.response

Aquí está la prueba. Lo importante aquí es crear la respuesta antes, ya que la función init no puede ser asíncrona y la llamada a getResponse es asíncrona, por lo que todo se verificó.

@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 tienes una mejor manera de hacerlo, dímelo pero creo que es bastante limpio así.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.