ฉันจะจำลองคำขอและการตอบสนองได้อย่างไร


222

ฉันกำลังพยายามใช้แพคเกจจำลอง Pythons เพื่อจำลองrequestsโมดูลPythons อะไรคือการโทรพื้นฐานเพื่อให้ฉันทำงานในสถานการณ์ด้านล่าง

ใน views.py ของฉันฉันมีฟังก์ชั่นที่ทำให้การร้องขอที่หลากหลายรับ () พร้อมการตอบสนองที่แตกต่างกันในแต่ละครั้ง

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

ในชั้นทดสอบของฉันฉันต้องการทำสิ่งนี้ แต่ไม่สามารถหาวิธีการโทรที่แน่นอนได้

ขั้นตอนที่ 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'

ขั้นตอนที่ 2:

เรียกมุมมองของฉัน

ขั้นตอนที่ 3:

ตรวจสอบการตอบสนองประกอบด้วย 'การตอบสนอง', 'การตอบสนองข', 'การตอบสนองค'

ฉันจะทำตามขั้นตอนที่ 1 (จำลองโมดูลคำขอ) ได้อย่างไร


5
นี่คือลิงค์การทำงานของ cra.mr/2014/05/20/mocking-requests-with-responses
Yogesh lele

คำตอบ:


278

นี่คือวิธีที่คุณสามารถทำได้ (คุณสามารถเรียกใช้ไฟล์นี้ตามที่เป็นอยู่):

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

หมายเหตุสำคัญ:หากMyGreatClassชั้นเรียนของคุณอยู่ในแพ็คเกจอื่นบอกว่าmy.great.packageคุณต้องเย้ยหยันmy.great.package.requests.getแทนที่จะเป็นเพียง 'request.get' ในกรณีนั้นกรณีทดสอบของคุณจะเป็นดังนี้:

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

สนุก!


2
คลาส MockResponse เป็นแนวคิดที่ยอดเยี่ยม! ฉันพยายามปลอม resuests วัตถุคลาสการตอบสนอง แต่มันไม่ง่าย ฉันสามารถใช้ MockResponse นี้แทนของจริงได้ ขอบคุณ!
yoshi

@yoshi ใช่ฉันใช้เวลาสักครู่เพื่อห่อหัวของฉันรอบ mocks ใน Python แต่มันใช้งานได้ค่อนข้างดีสำหรับฉัน!
Johannes Fahrenkrug

10
และใน Python 2.x เพียงแทนที่from unittest import mockด้วยimport mockส่วนที่เหลือก็ทำงานได้ตามปกติ คุณจำเป็นต้องติดตั้งmockแพ็คเกจแยกต่างหาก
haridsv

3
น่าอัศจรรย์ ฉันต้องทำการเปลี่ยนแปลงเล็กน้อยใน Python 3 ตามmock_requests_getต้องการyieldแทนที่จะเป็นreturnเพราะการเปลี่ยนเป็นตัววนซ้ำกลับใน Python 3
erip

1
นั่นคือสิ่งที่คำถามถูกถามในตอนแรก ฉันหาวิธีต่าง ๆ (เก็บแอปไว้ในแพ็คเกจและติดตั้ง test_client () เพื่อโทรออก) ขอบคุณสำหรับการโพสต์ แต่ยังคงใช้กระดูกสันหลังของรหัส
Suicide Bunny

142

ลองใช้ห้องสมุดการตอบสนอง นี่คือตัวอย่างจากเอกสารประกอบ :

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

มันให้ความสะดวกสบายที่ดีในการตั้งค่าการเยาะเย้ยตัวเองทั้งหมด

นอกจากนี้ยังมีHTTPretty :

มันไม่เฉพาะเจาะจงกับrequestsห้องสมุด แต่มีประสิทธิภาพมากกว่าในบางวิธี แต่ฉันพบว่ามันไม่ได้ยืมตัวเองได้ดีในการตรวจสอบคำขอที่ถูกดักฟังซึ่งresponsesทำได้ง่ายมาก

นอกจากนี้ยังมีhttmock


ฉันไม่เห็นหนทางที่responsesจะจับคู่ url ของ wildcard นั่นคือใช้ callback logic เช่น "ใช้ส่วนสุดท้ายของ url ค้นหาในแผนที่และคืนค่าที่สอดคล้องกัน" เป็นไปได้ไหมและฉันแค่คิดถึงมัน?
scubbo

1
@scubbo คุณสามารถส่ง regex ก่อนการคอมไพล์เป็น url param และใช้สไตล์การโทรกลับgithub.com/getsentry/responses#dynamic-responsesสิ่งนี้จะทำให้คุณมีพฤติกรรมตัวแทนที่คุณต้องการฉันคิดว่า (สามารถเข้าถึง url ที่ผ่านมาบนrequestARG ที่ได้รับจาก func โทรกลับ)
Anentropic

48

นี่คือสิ่งที่ทำงานให้ฉัน:

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
สิ่งนี้จะใช้งานได้หากคุณคาดหวังว่าจะได้รับข้อความ / html หากคุณกำลังเยาะเย้ย REST API ต้องการตรวจสอบรหัสสถานะ ฯลฯ คำตอบจากโยฮันเนส [ stackoverflow.com/a/28507806/3559967]น่าจะเป็นไปได้
Antony

5
สำหรับงูหลาม 3 from unittest import mockใช้ docs.python.org/3/library/unittest.mock.html
phoenix

33

ฉันใช้คำร้องขอจำลองสำหรับการเขียนการทดสอบสำหรับโมดูลแยก:

# module.py
import requests

class A():

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

และการทดสอบ:

# 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):'
Denis Evseev

16

นี่คือวิธีที่คุณจำลองคำร้องขอโพสต์เปลี่ยนเป็นวิธี 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
ถ้าฉันต้องการที่จะเยาะเย้ยฟังก์ชั่น? วิธีการเยาะเย้ยสิ่งนี้ตัวอย่างเช่น: mockresponse.json () = {"key": "value"}
primoz

1
@primoz ฉันใช้ฟังก์ชั่นที่ไม่ระบุชื่อ / แลมบ์ดาสำหรับเรื่องนั้น:mockresponse.json = lambda: {'key': 'value'}
Tayler

1
หรือmockresponse.json.return_value = {"key": "value"}
Lars Blumberg

5

หากคุณต้องการเยาะเย้ยการตอบกลับปลอมวิธีอื่นในการทำก็คือสร้างอินสแตนซ์ของคลาส HttpResponse พื้นฐานเช่น:

from django.http.response import HttpResponseBase

self.fake_response = HttpResponseBase()

นี่คือคำตอบสำหรับสิ่งที่ฉันพยายามค้นหา: รับวัตถุตอบสนอง django ปลอมที่สามารถผ่านขอบเขตของมิดเดิลแวร์สำหรับการทดสอบเกือบ e2e HttpResponseแทนที่จะทำ ... ฐานก็หลอกให้ฉัน ขอบคุณ!
low_ghost

4

วิธีหนึ่งที่เป็นไปได้ในการหลีกเลี่ยงคำขอคือการใช้ไลบรารี betamax มันจะบันทึกคำขอทั้งหมดและหลังจากนั้นหากคุณทำการร้องขอใน URL เดียวกันที่มีพารามิเตอร์เดียวกันกับที่ Betamax จะใช้คำขอที่บันทึกไว้ฉันใช้มันเพื่อทดสอบโปรแกรมรวบรวมข้อมูลเว็บ และช่วยฉันประหยัดเวลาได้มาก

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ได้รับการออกแบบมาเพื่อทำงานกับคำขอเท่านั้นหากคุณต้องการรวบรวมคำขอ HTTP ทำให้ผู้ใช้ HTTP API ระดับล่างเช่นhttplib3หรือaiohttpสำรองหรือ libs ไคลเอ็นต์อย่างboto …ใช้vcrpyแทนซึ่งทำงานในระดับต่ำกว่า อ่านเพิ่มเติมได้ที่github.com/betamaxpy/betamax/issues/125
Le Hibou

0

เพียงคำแนะนำที่เป็นประโยชน์สำหรับผู้ที่ยังคงดิ้นรนแปลงจาก urllib หรือ urllib2 / urllib3 เป็นการร้องขอและพยายามที่จะเยาะเย้ยการตอบสนอง - ฉันได้รับข้อผิดพลาดสับสนเล็กน้อยเมื่อใช้จำลองของฉัน:

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

AttributeError: __enter__

แน่นอนถ้าฉันรู้อะไรเกี่ยวกับวิธีการwithทำงาน (ฉันไม่ได้) ฉันรู้ว่ามันเป็นบริบทที่ไม่สำคัญและไม่จำเป็น(จากPEP 343 ) ไม่จำเป็นเมื่อใช้ห้องสมุดการร้องขอเพราะมันไม่พื้นเดียวกันสำหรับคุณภายใต้ประทุน เพียงแค่เอาwithและใช้เปลือยrequests.get(...)และบ๊อบเป็นลุงของคุณ


0

ฉันจะเพิ่มข้อมูลนี้เนื่องจากฉันมีเวลาหาวิธีการเยาะเย้ยเรียก async api

นี่คือสิ่งที่ฉันทำเพื่อเยาะเย้ยสาย async

นี่คือฟังก์ชั่นที่ฉันต้องการทดสอบ

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

คุณยังต้องการคลาส 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

คุณเพิ่มคลาส MockResponseAsync

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

    async def getResponse(self):
        return self.response

นี่คือการทดสอบ สิ่งสำคัญที่นี่คือฉันสร้างการตอบสนองก่อนเนื่องจากฟังก์ชั่นinitไม่สามารถเป็นแบบอะซิงก์ได้และการเรียกใช้ getResponse นั้นเป็นแบบอะซิงก์ดังนั้นจึงเช็คเอาต์ทั้งหมด

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

หากคุณมีวิธีที่ดีกว่าในการทำเช่นนั้นบอกฉัน แต่ฉันคิดว่ามันค่อนข้างสะอาดเช่นนั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.