Responses ========= .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master :target: https://travis-ci.org/getsentry/responses A utility library for mocking out the `requests` Python library. .. note:: Responses requires Python 2.7 or newer, and requests >= 2.0 Installing ---------- ``pip install responses`` Basics ------ The core of ``responses`` comes from registering mock responses: .. code-block:: python 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"}' If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise a ``ConnectionError``: .. code-block:: python import responses import requests from requests.exceptions import ConnectionError @responses.activate def test_simple(): with pytest.raises(ConnectionError): requests.get('http://twitter.com/api/1/foobar') Lastly, you can pass an ``Exception`` as the body to trigger an error on the request: .. code-block:: python import responses import requests @responses.activate def test_simple(): responses.add(responses.GET, 'http://twitter.com/api/1/foobar', body=Exception('...')) with pytest.raises(Exception): requests.get('http://twitter.com/api/1/foobar') Response Parameters ------------------- Responses are automatically registered via params on ``add``, but can also be passed directly: .. code-block:: python import responses responses.add( responses.Response( method='GET', url='http://example.com', ) ) The following attributes can be passed to a Response mock: method (``str``) The HTTP method (GET, POST, etc). url (``str`` or compiled regular expression) The full resource URL. match_querystring (``bool``) Include the query string when matching requests. Enabled by default if the response URL contains a query string, disabled if it doesn't or the URL is a regular expression. body (``str`` or ``BufferedReader``) The response body. json A Python object representing the JSON response body. Automatically configures the appropriate Content-Type. status (``int``) The HTTP status code. content_type (``content_type``) Defaults to ``text/plain``. headers (``dict``) Response headers. stream (``bool``) Disabled by default. Indicates the response should use the streaming API. Dynamic Responses ----------------- You can utilize callbacks to provide dynamic responses. The callback must return a tuple of (``status``, ``headers``, ``body``). .. code-block:: python import json import responses import requests @responses.activate def test_calc_api(): def request_callback(request): payload = json.loads(request.body) resp_body = {'value': sum(payload['numbers'])} headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} return (200, headers, json.dumps(resp_body)) responses.add_callback( responses.POST, 'http://calc.com/sum', callback=request_callback, content_type='application/json', ) resp = requests.post( 'http://calc.com/sum', json.dumps({'numbers': [1, 2, 3]}), headers={'content-type': 'application/json'}, ) assert resp.json() == {'value': 6} assert len(responses.calls) == 1 assert responses.calls[0].request.url == 'http://calc.com/sum' assert responses.calls[0].response.text == '{"value": 6}' assert ( responses.calls[0].response.headers['request-id'] == '728d329e-0e86-11e4-a748-0c84dc037c13' ) You can also pass a compiled regex to `add_callback` to match multiple urls: .. code-block:: python import re, json from functools import reduce import responses import requests operators = { 'sum': lambda x, y: x+y, 'prod': lambda x, y: x*y, 'pow': lambda x, y: x**y } @responses.activate def test_regex_url(): def request_callback(request): payload = json.loads(request.body) operator_name = request.path_url[1:] operator = operators[operator_name] resp_body = {'value': reduce(operator, payload['numbers'])} headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} return (200, headers, json.dumps(resp_body)) responses.add_callback( responses.POST, re.compile('http://calc.com/(sum|prod|pow|unsupported)'), callback=request_callback, content_type='application/json', ) resp = requests.post( 'http://calc.com/prod', json.dumps({'numbers': [2, 3, 4]}), headers={'content-type': 'application/json'}, ) assert resp.json() == {'value': 24} test_regex_url() If you want to pass extra keyword arguments to the callback function, for example when reusing a callback function to give a slightly different result, you can use ``functools.partial``: .. code-block:: python from functools import partial ... def request_callback(request, id=None): payload = json.loads(request.body) resp_body = {'value': sum(payload['numbers'])} headers = {'request-id': id} return (200, headers, json.dumps(resp_body)) responses.add_callback( responses.POST, 'http://calc.com/sum', callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'), content_type='application/json', ) Responses as a context manager ------------------------------ .. code-block:: python import responses import requests def test_my_api(): with responses.RequestsMock() as rsps: rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', body='{}', status=200, content_type='application/json') resp = requests.get('http://twitter.com/api/1/foobar') assert resp.status_code == 200 # outside the context manager requests will hit the remote server resp = requests.get('http://twitter.com/api/1/foobar') resp.status_code == 404 Responses as a pytest fixture ----------------------------- .. code-block:: python @pytest.fixture def mocked_responses(): with responses.RequestsMock() as rsps: yield rsps def test_api(mocked_responses): mocked_responses.add( responses.GET, 'http://twitter.com/api/1/foobar', body='{}', status=200, content_type='application/json') resp = requests.get('http://twitter.com/api/1/foobar') assert resp.status_code == 200 Assertions on declared responses -------------------------------- When used as a context manager, Responses will, by default, raise an assertion error if a url was registered but not accessed. This can be disabled by passing the ``assert_all_requests_are_fired`` value: .. code-block:: python import responses import requests def test_my_api(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', body='{}', status=200, content_type='application/json') Multiple Responses ------------------ You can also add multiple responses for the same url: .. code-block:: python import responses import requests @responses.activate def test_my_api(): responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) responses.add(responses.GET, 'http://twitter.com/api/1/foobar', body='{}', status=200, content_type='application/json') resp = requests.get('http://twitter.com/api/1/foobar') assert resp.status_code == 500 resp = requests.get('http://twitter.com/api/1/foobar') assert resp.status_code == 200 Using a callback to modify the response --------------------------------------- If you use customized processing in `requests` via subclassing/mixins, or if you have library tools that interact with `requests` at a low level, you may need to add extended processing to the mocked Response object to fully simulate the environment for your tests. A `response_callback` can be used, which will be wrapped by the library before being returned to the caller. The callback accepts a `response` as it's single argument, and is expected to return a single `response` object. .. code-block:: python import responses import requests def response_callback(resp): resp.callback_processed = True return resp with responses.RequestsMock(response_callback=response_callback) as m: m.add(responses.GET, 'http://example.com', body=b'test') resp = requests.get('http://example.com') assert resp.text == "test" assert hasattr(resp, 'callback_processed') assert resp.callback_processed is True Passing thru real requests -------------------------- In some cases you may wish to allow for certain requests to pass thru responses and hit a real server. This can be done with the 'passthru' methods: .. code-block:: python import responses @responses.activate def test_my_api(): responses.add_passthru('https://percy.io') This will allow any requests matching that prefix, that is otherwise not registered as a mock response, to passthru using the standard behavior. Viewing/Modifying registered responses -------------------------------------- Registered responses are available as a private attribute of the RequestMock instance. It is sometimes useful for debugging purposes to view the stack of registered responses which can be accessed via ``responses.mock._matches``. The ``replace`` function allows a previously registered ``response`` to be changed. The method signature is identical to ``add``. ``response``s are identified using ``method`` and ``url``. Only the first matched ``response`` is replaced. .. code-block:: python import responses import requests @responses.activate def test_replace(): responses.add(responses.GET, 'http://example.org', json={'data': 1}) responses.replace(responses.GET, 'http://example.org', json={'data': 2}) resp = requests.get('http://example.org') assert resp.json() == {'data': 2} ``remove`` takes a ``method`` and ``url`` argument and will remove *all* matched ``response``s from the registered list. Finally, ``clear`` will reset all registered ``response``s Contributing ------------ Responses uses several linting and autoformatting utilities, so it's important that when submitting patches you use the appropriate toolchain: Clone the repository: .. code-block:: shell git clone https://github.com/getsentry/responses.git Create an environment (e.g. with ``virtualenv``): .. code-block:: shell virtualenv .env && source .env/bin/activate Configure development requirements: .. code-block:: shell make develop