A utility library for mocking out the requests Python library.
Note
Responses requires Python 2.7 or newer, and requests >= 2.0
pip install responses
The core of responses comes from registering mock responses:
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:
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:
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')Responses are automatically registered via params on add, but can also be
passed directly:
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 (
stror 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 (
strorBufferedReader) - 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.
You can utilize callbacks to provide dynamic responses. The callback must return
a tuple of (status, headers, body).
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'
)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:
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',
)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@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 == 200When 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:
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')You can also add multiple responses for the same url:
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 == 200If 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.
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 TrueIn 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:
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.
Responses uses several linting and autoformatting utilities, so it's important that when submitting patches you use the appropriate toolchain:
Clone the repository:
git clone https://github.com/getsentry/responses.gitCreate an environment (e.g. with virtualenv):
virtualenv .env && source .env/bin/activateConfigure development requirements:
make develop