요청과 응답을 어떻게 조롱 할 수 있습니까?


221

Pythons mock package 를 사용 하여 Pythons requests모듈 을 조롱 하려고 합니다. 아래 시나리오에서 저를 일하게하는 기본 전화는 무엇입니까?

views.py에는 매번 다른 응답으로 다양한 requests.get () 호출을 수행하는 함수가 있습니다.

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 단계 :

응답에 'a response', 'b response', 'c response'가 포함되어 있는지 확인

1 단계 (요청 모듈 모의)를 완료하려면 어떻게해야합니까?


답변:


277

이것은 당신이 그것을 할 수있는 방법입니다 (이 파일을 그대로 실행할 수 있습니다) :

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 클래스가 다른 패키지에 거주하는 경우 'request.get'대신 my.great.package모의해야합니다 my.great.package.requests.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 수업은 좋은 생각입니다! 이력서를 속이려고했지만 응답 클래스 객체이지만 쉽지 않았습니다. 이 대신 MockResponse를 사용할 수 있습니다. 감사합니다!
요시

@yoshi 그래, 파이썬에서 모의 ​​머리를 감싸는 데 시간이 걸렸지 만 이것은 나에게 꽤 잘 작동한다!
Johannes Fahrenkrug

10
그리고 파이썬 2.X에서 바로 교체 from unittest import mockimport mock하고 나머지는 그대로 작동합니다. mock패키지를 별도로 설치해야합니다 .
haridsv

3
환상적인. 파이썬 3 에서 이터레이터를 반환하는 대신 에 파이썬 3을 약간 변경 mock_requests_get해야했습니다.yieldreturn
erip

1
그것이 그 질문이 원래 요구했던 것입니다. 방법을 알아 냈습니다 (앱을 패키지에 넣고 test_client ()를 호출하여 호출하십시오). 그래도 게시물 덕분에 여전히 코드의 백본을 사용하고있었습니다.
자살 토끼

141

응답 라이브러리를 사용해보십시오 .

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과 일치 하는 방법을 보지 못했습니다. 즉, "URL의 마지막 부분을 가져 와서 맵에서 찾아 해당 값을 반환합니다"와 같은 콜백 로직을 구현합니다. 가능합니까, 나는 그것을 놓치고 있습니까?
scubbo

1
@ scubbo url 매개 변수로 미리 컴파일 된 정규 표현식을 전달하고 콜백 스타일 github.com/getsentry/responses#dynamic-responses를 사용 하면 원하는 와일드 카드 동작을 얻을 수 있습니다 ( requestarg 에서 전달 된 URL에 액세스 할 수 있음) 콜백 기능에 의해 수신)
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를 조롱하고 상태 코드 등을 확인하려는 경우 Johannes [ stackoverflow.com/a/28507806/3559967] 의 답변 이 좋습니다.
Antony

5
Python 3의 경우을 사용하십시오 from unittest import mock. docs.python.org/3/library/unittest.mock.html
피닉스

32

내가 사용 요청 - 모의을 별도의 모듈에 대한 테스트를 작성을 위해 :

# 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)'에서 m을 어디서 얻습니까?
Denis

16

이것은 requests.post를 조롱하는 방법입니다 .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()

이것은 내가 찾은 것에 대한 대답입니다. 거의 e2e 테스트를 위해 미들웨어의 영역을 통해 그것을 만들 수있는 가짜 장고 응답 객체를 얻으십시오. HttpResponse... Base보다는 오히려 나를 위해 속임수를 사용했습니다. 감사!
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/


참고 베타 맥스가 단지로 설계되어 작동 요청 은 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

비동기 API 호출을 조롱하는 방법을 알아내는 데 어려움을 겪었 으므로이 정보를 추가 할 것입니다.

다음은 비동기 호출을 조롱하기 위해 수행 한 작업입니다.

테스트하고 싶은 기능은 다음과 같습니다.

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.