Bagaimana saya bisa mengejek permintaan dan responsnya?

221

Saya mencoba menggunakan paket Python mock untuk mengejek requestsmodul Python . Apa saja panggilan dasar untuk membuat saya bekerja dalam skenario di bawah ini?

Dalam views.py saya, saya memiliki fungsi yang membuat berbagai permintaan. Get () panggilan dengan respons berbeda setiap kali

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

Di kelas pengujian saya, saya ingin melakukan sesuatu seperti ini tetapi tidak dapat menemukan panggilan metode yang tepat

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

Langkah 2:

Panggil pandanganku

Langkah 3:

verifikasi respons mengandung 'respons', 'respons b', 'respons c'

Bagaimana saya bisa menyelesaikan Langkah 1 (mengejek modul permintaan)?

kk1957
sumber
5
Inilah tautan yang berfungsi cra.mr/2014/05/20/mocking-requests-with-responses
Yogesh lele

Jawaban:

277

Ini adalah bagaimana Anda dapat melakukannya (Anda dapat menjalankan file ini apa adanya):

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

Catatan Penting: Jika MyGreatClasskelas Anda hidup dalam paket yang berbeda, katakanlah my.great.package, Anda harus mengejek, my.great.package.requests.getbukan hanya 'request.get'. Dalam hal ini, ujian Anda akan terlihat seperti ini:

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

Nikmati!

Johannes Fahrenkrug
sumber
2
Kelas MockResponse adalah ide bagus! Saya mencoba memalsukan objek resuests.Response kelas tapi itu tidak mudah. Saya bisa menggunakan MockResponse ini sebagai pengganti yang asli. Terima kasih!
yoshi
@Yoshi Ya, butuh beberapa saat untuk membungkus kepala saya dengan cemoohan di Python tapi ini bekerja cukup baik untuk saya!
Johannes Fahrenkrug
10
Dan dalam Python 2.x, ganti saja from unittest import mockdengan import mockdan sisanya berfungsi sebagaimana mestinya. Anda perlu menginstal mockpaket secara terpisah.
haridsv
3
Fantastis. Saya harus membuat sedikit perubahan pada Python 3 sesuai mock_requests_getkebutuhan yieldalih-alih returnkarena perubahan untuk mengembalikan iterator dengan Python 3.
erip
1
itulah pertanyaan yang awalnya ditanyakan. Saya telah menemukan cara (masukkan aplikasi ke dalam paket dan buat test_client () untuk melakukan panggilan). terima kasih untuk postingnya, masih menggunakan tulang punggung kode.
Suicide Bunny
141

Coba gunakan perpustakaan tanggapan :

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

memberikan kenyamanan yang cukup baik atas pengaturan semua mengejek diri sendiri

Ada juga HTTPretty :

Ini tidak spesifik untuk requestsperpustakaan, lebih kuat dalam beberapa hal meskipun saya menemukan itu tidak cocok untuk memeriksa permintaan yang disadap, yang responsestidak cukup mudah

Ada juga httmock .

Anentropik
sumber
Sekilas, saya tidak melihat cara untuk responsesmencocokkan url wildcard - yaitu, menerapkan logika panggil balik seperti "ambil bagian terakhir dari url, cari di Peta, dan kembalikan nilai yang sesuai". Apakah itu mungkin, dan saya hanya melewatkannya?
scubbo
1
@scubbo Anda dapat melewati regex yang telah dikompilasi sebelumnya sebagai parameter url dan menggunakan gaya panggilan balik github.com/getsentry/responses#dynamic-responses ini akan memberi Anda perilaku wildcard yang Anda inginkan menurut saya (dapat mengakses url yang lulus pada requestargumen diterima oleh fungsi panggilan balik)
Anentropic
48

Inilah yang bekerja untuk saya:

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
sumber
3
Ini akan berfungsi jika Anda mengharapkan respons teks / html. Jika Anda mengejek API REST, ingin memeriksa kode status, dll. Maka jawaban dari Johannes [ stackoverflow.com/a/28507806/3559967] mungkin merupakan cara yang harus dilakukan.
Antony
5
Untuk Python 3, gunakan from unittest import mock. docs.python.org/3/library/unittest.mock.html
phoenix
32

Saya menggunakan permintaan-mock untuk menulis tes untuk modul terpisah:

# module.py
import requests

class A():

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

Dan tesnya:

# 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
sumber
Di mana Anda mendapatkan m '(mandiri, m):'
Denis Evseev
16

ini adalah bagaimana Anda mengejek requests.post, ubah ke metode http Anda

@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
sumber
1
Bagaimana jika saya ingin mengejek suatu fungsi? Bagaimana mengejek ini misalnya: mockresponse.json () = {"key": "value"}
primoz
1
@ primoz, saya menggunakan fungsi anonim / lambda untuk itu:mockresponse.json = lambda: {'key': 'value'}
Tayler
1
Ataumockresponse.json.return_value = {"key": "value"}
Lars Blumberg
5

Jika Anda ingin mengejek respons palsu, cara lain untuk melakukannya adalah dengan hanya membuat instance instance dari kelas HttpResponse kelas, seperti:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()
Tom Chapin
sumber
Ini adalah jawaban untuk apa yang saya coba temukan: dapatkan objek tanggapan Django palsu yang dapat membuatnya melalui keseluruhan middleware untuk tes yang hampir e2e. HttpResponse, bukannya ... Base, melakukan trik untukku. Terima kasih!
low_ghost
4

Salah satu cara yang mungkin untuk mengatasi permintaan adalah menggunakan perpustakaan betamax, itu mencatat semua permintaan dan setelah itu jika Anda membuat permintaan dalam url yang sama dengan parameter yang sama betamax akan menggunakan permintaan yang direkam, saya telah menggunakannya untuk menguji crawler web dan itu menghemat banyak waktu.

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
sumber
Perhatikan bahwa betamax dirancang hanya berfungsi dengan permintaan , jika Anda perlu menangkap permintaan HTTP yang membuat pengguna HTTP API tingkat rendah seperti httplib3 , atau dengan aiohttp alternatif , atau lib klien seperti boto ... gunakan vcrpy sebagai gantinya yang bekerja di tingkat bawah. Lebih banyak di github.com/betamaxpy/betamax/issues/125
Le Hibou
0

Hanya sebuah petunjuk bermanfaat bagi mereka yang masih berjuang, mengubah dari urllib atau urllib2 / urllib3 ke permintaan DAN mencoba untuk mengejek tanggapan - Saya mendapatkan kesalahan yang sedikit membingungkan ketika menerapkan tiruan saya:

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

AttributeError: __enter__

Ya, tentu saja, jika saya tahu apa-apa tentang cara withkerjanya (saya tidak), saya akan tahu itu adalah konteks yang tidak penting dan tidak perlu (dari PEP 343 ). Tidak perlu saat menggunakan pustaka permintaan karena pada dasarnya melakukan hal yang sama untuk Anda di bawah tenda . Hapus withdan gunakan telanjang requests.get(...)dan Bob pamanmu .

Max P Magee
sumber
0

Saya akan menambahkan informasi ini karena saya mengalami kesulitan mencari cara untuk mengejek panggilan api async.

Inilah yang saya lakukan untuk mengejek panggilan async.

Inilah fungsi yang ingin saya uji

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

Anda masih membutuhkan kelas 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

Anda menambahkan kelas MockResponseAsync

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

    async def getResponse(self):
        return self.response

Ini tesnya. Yang penting di sini adalah saya membuat respons sebelumnya karena fungsi init tidak dapat async dan panggilan untuk getResponse adalah async sehingga semuanya diperiksa.

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

Jika Anda memiliki cara yang lebih baik untuk melakukannya, beri tahu saya, tetapi saya pikir itu cukup bersih seperti itu.

Martbob
sumber