İstekleri ve yanıtı nasıl alay edebilirim?


223

Pythons modülünü alay etmek için Pythons sahte paketini kullanmaya çalışıyorum requests. Aşağıdaki senaryoda çalışmamı sağlayacak temel çağrılar nelerdir?

Benim görünümlerimde, her seferinde farklı yanıtlarla çeşitli requests.get () çağrıları yapan bir fonksiyonum var

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

Test sınıfımda böyle bir şey yapmak istiyorum ancak kesin yöntem çağrılarını anlayamıyorum

Aşama 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'

Adım 2:

Görüşümü ara

Aşama 3:

yanıtın 'yanıt', 'b yanıt', 'c yanıt' içerdiğini doğrulayın

Adım 1'i nasıl tamamlayabilirim (istek modülü ile alay etmek)?


Yanıtlar:


278

Bunu şu şekilde yapabilirsiniz (bu dosyayı olduğu gibi çalıştırabilirsiniz):

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

Önemli Not: senin Eğer MyGreatClassfarklı bir pakette sınıf hayatlarını, demek my.great.package, sen alay etmek zorunda my.great.package.requests.getbunun yerine sadece 'request.get' arasında. Bu durumda test durumunuz şöyle görünür:

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

Zevk almak!


2
MockResponse sınıfı harika bir fikir! Resuests.Response sınıf nesnesini taklit etmeye çalışıyordum ama kolay olmadı. Bu MockResponse'yi gerçek şey yerine kullanabilirim. Teşekkür ederim!
yoshi

@yoshi Evet, kafamı Python'daki alayların etrafına sarmak biraz zaman aldı ama bu benim için oldukça iyi çalışıyor!
Johannes Fahrenkrug

10
Ve Python 2.x içinde, sadece yerini from unittest import mockile import mockve geri kalanı olarak çalışmaktadır. mockPaketi ayrı olarak kurmanız gerekir .
haridsv

3
Fantastik. Ben Python 3 hafif bir değişiklik yapmak zorunda kaldı mock_requests_getgerekiyordu yieldyerine returnPython 3. yineleyicinızı dönen nedeniyle değişim
erip

1
sorunun asıl sorduğu şey buydu. Ben yollar anladım (uygulamayı paket içine paketi ve arama yapmak için bir test_client () fikstür). yazı için teşekkürler, yine de hala kod omurgasını kullanıyordu.
Suicide Bunny

142

Yanıt kitaplığını kullanmayı deneyin . Dokümanlarından bir örnek :

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

Tüm alaycılığı kurma konusunda oldukça güzel bir kolaylık sağlar.

Ayrıca HTTPretty de var :

requestsKütüphaneye özgü değil , bazı yönlerden daha güçlü olsa da, kesilen istekleri teftiş etmek için çok iyi ödünç vermediğini fark ettim, bu responsesda oldukça kolay

Ayrıca httmock var .


Bir bakışta, responsesjoker karakterli bir URL ile eşleşmenin bir yolunu görmedim - yani, "url'nin son kısmını al, bir Haritaya bak ve karşılık gelen değeri döndür" gibi geri arama mantığı uygulayın. Bu mümkün mü ve sadece özlüyorum?
scubbo

1
@scubbo, url parametresi olarak önceden derlenmiş bir normal ifadeyi iletebilir ve github.com/getsentry/responses#dynamic-responses geri arama stilini kullanabilir, bu da size istediğiniz joker davranışı verecektir ( requestarg üzerinden iletilen URL'ye erişebilir) (geri arama
fonu

48

İşte benim için işe yarayan:

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
Metin / html yanıtları bekliyorsanız bu işe yarar. Bir REST API'sini taklit ediyorsanız, durum kodunu vb. Kontrol etmek istiyorsanız, Johannes'in cevabı [ stackoverflow.com/a/28507806/3559967] muhtemelen yoludur.
Antony

5
Python 3 için kullanın from unittest import mock. docs.python.org/3/library/unittest.mock.html
phoenix

33

Ayrı bir modül için test yazma istekleri-sahte kullandım :

# module.py
import requests

class A():

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

Ve testler:

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

'(Self, m)' de nereden m alırsınız:
Denis Evseev

16

requests.post, bu şekilde alay, http yönteminize değiştirin

@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
Bir işlevi alay etmek istersem ne olur? Bunu nasıl alay edebilirim: mockresponse.json () = {"key": "value"}
primoz

1
@primoz, bunun için anonim bir işlev / lambda kullandım:mockresponse.json = lambda: {'key': 'value'}
Tayler

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

5

Sahte bir yanıtı taklit etmek istiyorsanız, bunu yapmanın başka bir yolu, temel HttpResponse sınıfının bir örneğini örneklemektir:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

Bu bulmaya çalıştığım şeyin cevabı: neredeyse e2e testi için ara katman yazılımı ile yapabilen sahte bir django yanıt nesnesi al. HttpResponse... yerine, üsse benim için hile yaptı. Teşekkürler!
low_ghost

4

İstekleri geçici olarak çözmek için olası bir yol, kütüphane betamax kullanmaktır, tüm istekleri kaydeder ve bundan sonra aynı url'de aynı parametrelerle bir istek yaparsanız, betamax kaydedilen isteği kullanırsa, web tarayıcıyı test etmek için kullanıyorum ve bu bana çok zaman kazandırıyor.

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/


Betamax'ın yalnızca aşağıdakilerle çalışacak şekilde tasarlandığını unutmayın: istekleri HTTP yakalamak gerekiyorsa, gibi kullanıcı alt düzey HTTP API yapılan istekleri httplib3 veya alternatif ile aiohttp gibi veya müşteri kütüphanelerini boto ... kullanım vcrpy alt seviyede çalışır yerine. Daha fazlası için github.com/betamaxpy/betamax/issues/125
Le Hibou

0

Hala mücadele edenlere, urllib veya urllib2 / urllib3'ten isteklere dönüşen VE bir yanıtı alay etmeye çalışanlara sadece yararlı bir ipucu - Alayımı uygularken biraz kafa karıştırıcı bir hata alıyordum:

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

Özellik Hatası: __enter__

Tabii ki, nasıl withçalıştığına dair bir şey biliyorsam (yapmadım), bunun körelmiş, gereksiz bir bağlam olduğunu bilirdim ( PEP 343'ten ). Temelde başlık altında sizin için aynı şeyi yaptığı için istek kitaplığını kullanırken gereksizdir . Sadece çıkarın withve çıplak kullanın requests.get(...)ve Bob amcanız .


0

Bir async api çağrısı ile alay etmeyi anlamaya zorlandığım için bu bilgiyi ekleyeceğim.

İşte bir zaman uyumsuz çağrı alay etmek için yaptım.

İşte test etmek istediğim işlev

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

Yine de MockResponse sınıfına ihtiyacınız var

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

MockResponseAsync sınıfını eklersiniz

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

    async def getResponse(self):
        return self.response

İşte test. Burada önemli olan, daha önce init işlevinin zaman uyumsuz olamayacağı ve getResponse çağrısının zaman uyumsuz olduğu için daha .

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

Bunu yapmanın daha iyi bir yolu varsa bana söyle ama bence böyle temiz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.