Como posso simular solicitações e a resposta?

221

Estou tentando usar o pacote simulado Pythons para simular o requestsmódulo Pythons . Quais são as ligações básicas para que eu trabalhe no cenário abaixo?

No meu views.py, tenho uma função que faz várias chamadas orders.get () com respostas diferentes a cada vez

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

Na minha classe de teste, quero fazer algo assim, mas não consigo descobrir exatamente as chamadas de método

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

Passo 2:

Chamar minha opinião

Etapa 3:

verificar se a resposta contém 'uma resposta', 'b resposta', 'c resposta'

Como concluir a Etapa 1 (zombando do módulo de solicitações)?

kk1957
fonte
5
Aqui está o link de trabalho cra.mr/2014/05/20/mocking-requests-with-responses
Yogesh lele

Respostas:

277

É assim que você pode fazer (você pode executar este arquivo como está):

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: Se a sua MyGreatClassclasse mora em um pacote diferente, digamos my.great.package, você precisa zombar em my.great.package.requests.getvez de apenas 'request.get'. Nesse caso, seu caso de teste ficaria assim:

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

Aproveitar!

Johannes Fahrenkrug
fonte
2
A classe MockResponse é uma ótima ideia! Eu estava tentando falsificar um objeto de classe Resuests.Response, mas não foi fácil. Eu poderia usar esse MockResponse no lugar da coisa real. Obrigado!
precisa
@yoshi Sim, demorei um pouco para envolver minha cabeça em piadas em Python, mas isso funciona muito bem para mim!
Johannes Fahrenkrug
10
E no Python 2.x, basta substituir from unittest import mockpor import mocke o resto funciona como está. Você precisa instalar o mockpacote separadamente.
haridsv
3
Fantástico. Eu tive que fazer uma pequena alteração no Python 3 como mock_requests_getnecessário para yield, em vez de returnpor causa da mudança para voltar iterators em Python 3.
erip
1
era sobre isso que a pergunta estava perguntando originalmente. Eu descobri maneiras (empacote o aplicativo no pacote e instale um test_client () para fazer a chamada). obrigado pelo post, no entanto, ainda estava usando a espinha dorsal do código.
9789 Bunny Suicide Bunny
141

Tente usar a biblioteca de respostas :

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

fornece uma conveniência bastante agradável ao configurar toda a zombaria de si mesmo

Há também HTTPretty :

Não é específico para a requestsbiblioteca, é mais poderoso em alguns aspectos, embora eu ache que não se presta tão bem para inspecionar os pedidos que interceptou, o que responsesé bastante fácil

Há também um resetmock .

Anentrópico
fonte
De relance, não encontrei uma maneira de responsescorresponder a um URL curinga - ou seja, implemente a lógica de retorno de chamada como "pegue a última parte do URL, procure-o em um mapa e retorne o valor correspondente". Isso é possível e só estou sentindo falta disso?
Scubbo 6/01/19
1
@scubbo, você pode passar um regex pré-compilado como o parâmetro url e usar o estilo de retorno de chamada github.com/getsentry/responses#dynamic-responses. Isso fornecerá o comportamento curinga que você deseja que eu ache (pode acessar o URL passado no requestarg recebido pela função de retorno de chamada)
Anentropic
48

Aqui está o que funcionou para mim:

import mock
@mock.patch('requests.get', mock.Mock(side_effect = lambda k:{'aurl': 'a response', 'burl' : 'b response'}.get(k, 'unhandled request %s'%k)))
kk1957
fonte
3
Isso funcionará se você estiver esperando respostas de texto / html. Se você estiver zombando de uma API REST, quiser verificar o código de status etc., a resposta de Johannes [ stackoverflow.com/a/28507806/3559967] provavelmente é o caminho a percorrer.
Antony
5
Para Python 3, use from unittest import mock. docs.python.org/3/library/unittest.mock.html
phoenix
32

Eu usei orders-mock para escrever testes para o módulo separado:

# module.py
import requests

class A():

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

E os testes:

# 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()
AnaPana
fonte
Onde você consegue m in '(self, m):'
Denis Evseev 16/06
16

é assim que você zomba de orders.post, altera para o 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
tingyiy
fonte
1
E se eu quiser zombar de uma função? Como a zombar esta por exemplo: mockresponse.json () = { "chave": "Valor"}
Primoz
1
@primoz, eu usei uma função / lambda anônima para isso: #mockresponse.json = lambda: {'key': 'value'}
Tayler
1
Oumockresponse.json.return_value = {"key": "value"}
Lars Blumberg
5

Se você quiser zombar de uma resposta falsa, outra maneira de fazer isso é simplesmente instanciar uma instância da classe base HttpResponse, da seguinte maneira:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()
Tom Chapin
fonte
Esta é a resposta para o que eu estava tentando encontrar: obter um objeto de resposta django falso que possa passar pela gama de middlewares para um teste quase e2e. HttpResponse, em vez de ... Base, fez o truque para mim. Obrigado!
low_ghost
4

Uma maneira possível de contornar solicitações é usar a biblioteca betamax, ela registra todas as solicitações e, depois disso, se você fizer uma solicitação no mesmo URL com os mesmos parâmetros, a betamax usará a solicitação gravada, eu a uso para testar o rastreador da Web e isso me poupa muito tempo.

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/

Ronald Theodoro
fonte
Observe que o betamax foi projetado para funcionar apenas com solicitações , se você precisar capturar solicitações HTTP feitas por APIs de nível inferior ao usuário, como activationplib3 , ou com aiohttp alternativo , ou bibliotecas de clientes como boto … use vcrpy, que funciona em nível inferior. Mais em github.com/betamaxpy/betamax/issues/125
Le Hibou
0

Apenas uma dica útil para aqueles que ainda estão lutando, convertendo de urllib ou urllib2 / urllib3 em solicitações E tentando zombar de uma resposta - eu estava recebendo um erro um pouco confuso ao implementar meu mock:

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

AttributeError: __enter__

Bem, é claro, se eu soubesse alguma coisa sobre como withfunciona (não sabia), saberia que era um contexto vestigial e desnecessário (do PEP 343 ). Desnecessário quando usando a biblioteca pedidos porque ele faz basicamente a mesma coisa para você sob o capô . Basta remover withe usar o nu requests.get(...)e Bob é seu tio .

Max P Magee
fonte
0

Adicionarei essas informações, já que tive dificuldade em descobrir como zombar de uma chamada de API assíncrona.

Aqui está o que eu fiz para zombar de uma chamada assíncrona.

Aqui está a função que eu queria testar

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

Você ainda precisa da 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

Você adiciona a 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

Aqui está o teste. O importante aqui é que eu criei a resposta antes, já que a função init não pode ser assíncrona e a chamada para getResponse é assíncrona, portanto tudo foi verificado.

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

Se você tem uma maneira melhor de fazer isso, me diga, mas acho que é bem limpo assim.

Martbob
fonte