123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521 |
- Responses
- =========
- .. image:: https://img.shields.io/pypi/v/responses.svg
- :target: https://pypi.python.org/pypi/responses/
- .. image:: https://img.shields.io/pypi/pyversions/responses.svg
- :target: https://pypi.org/project/responses/
- .. image:: https://img.shields.io/pypi/dm/responses
- :target: https://pypi.python.org/pypi/responses/
- .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg
- :target: https://codecov.io/gh/getsentry/responses/
- A utility library for mocking out the ``requests`` Python library.
- .. note::
- Responses requires Python 3.8 or newer, and requests >= 2.30.0
- Table of Contents
- -----------------
- .. contents::
- Installing
- ----------
- ``pip install responses``
- Deprecations and Migration Path
- -------------------------------
- Here you will find a list of deprecated functionality and a migration path for each.
- Please ensure to update your code according to the guidance.
- .. list-table:: Deprecation and Migration
- :widths: 50 25 50
- :header-rows: 1
- * - Deprecated Functionality
- - Deprecated in Version
- - Migration Path
- * - ``responses.json_params_matcher``
- - 0.14.0
- - ``responses.matchers.json_params_matcher``
- * - ``responses.urlencoded_params_matcher``
- - 0.14.0
- - ``responses.matchers.urlencoded_params_matcher``
- * - ``stream`` argument in ``Response`` and ``CallbackResponse``
- - 0.15.0
- - Use ``stream`` argument in request directly.
- * - ``match_querystring`` argument in ``Response`` and ``CallbackResponse``.
- - 0.17.0
- - Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher``
- * - ``responses.assert_all_requests_are_fired``, ``responses.passthru_prefixes``, ``responses.target``
- - 0.20.0
- - Use ``responses.mock.assert_all_requests_are_fired``,
- ``responses.mock.passthru_prefixes``, ``responses.mock.target`` instead.
- Basics
- ------
- The core of ``responses`` comes from registering mock responses and covering test function
- with ``responses.activate`` decorator. ``responses`` provides similar interface as ``requests``.
- Main Interface
- ^^^^^^^^^^^^^^
- * responses.add(``Response`` or ``Response args``) - allows either to register ``Response`` object or directly
- provide arguments of ``Response`` object. See `Response Parameters`_
- .. code-block:: python
- import responses
- import requests
- @responses.activate
- def test_simple():
- # Register via 'Response' object
- rsp1 = responses.Response(
- method="PUT",
- url="http://example.com",
- )
- responses.add(rsp1)
- # register via direct arguments
- 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")
- resp2 = requests.put("http://example.com")
- assert resp.json() == {"error": "not found"}
- assert resp.status_code == 404
- assert resp2.status_code == 200
- assert resp2.request.method == "PUT"
- 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")
- Shortcuts
- ^^^^^^^^^
- Shortcuts provide a shorten version of ``responses.add()`` where method argument is prefilled
- * responses.delete(``Response args``) - register DELETE response
- * responses.get(``Response args``) - register GET response
- * responses.head(``Response args``) - register HEAD response
- * responses.options(``Response args``) - register OPTIONS response
- * responses.patch(``Response args``) - register PATCH response
- * responses.post(``Response args``) - register POST response
- * responses.put(``Response args``) - register PUT response
- .. code-block:: python
- import responses
- import requests
- @responses.activate
- def test_simple():
- responses.get(
- "http://twitter.com/api/1/foobar",
- json={"type": "get"},
- )
- responses.post(
- "http://twitter.com/api/1/foobar",
- json={"type": "post"},
- )
- responses.patch(
- "http://twitter.com/api/1/foobar",
- json={"type": "patch"},
- )
- resp_get = requests.get("http://twitter.com/api/1/foobar")
- resp_post = requests.post("http://twitter.com/api/1/foobar")
- resp_patch = requests.patch("http://twitter.com/api/1/foobar")
- assert resp_get.json() == {"type": "get"}
- assert resp_post.json() == {"type": "post"}
- assert resp_patch.json() == {"type": "patch"}
- Responses as a context manager
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Instead of wrapping the whole function with decorator you can use 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
- Response Parameters
- -------------------
- 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``)
- DEPRECATED: Use ``responses.matchers.query_param_matcher`` or
- ``responses.matchers.query_string_matcher``
- 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`` or ``Exception``)
- The response body. Read more `Exception as 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``)
- DEPRECATED: use ``stream`` argument in request directly
- auto_calculate_content_length (``bool``)
- Disabled by default. Automatically calculates the length of a supplied string or JSON body.
- match (``tuple``)
- An iterable (``tuple`` is recommended) of callbacks to match requests
- based on request attributes.
- Current module provides multiple matchers that you can use to match:
- * body contents in JSON format
- * body contents in URL encoded data format
- * request query parameters
- * request query string (similar to query parameters but takes string as input)
- * kwargs provided to request e.g. ``stream``, ``verify``
- * 'multipart/form-data' content and headers in request
- * request headers
- * request fragment identifier
- Alternatively user can create custom matcher.
- Read more `Matching Requests`_
- Exception as Response body
- --------------------------
- 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.get("http://twitter.com/api/1/foobar", body=Exception("..."))
- with pytest.raises(Exception):
- requests.get("http://twitter.com/api/1/foobar")
- Matching Requests
- -----------------
- Matching Request Body Contents
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- When adding responses for endpoints that are sent request data you can add
- matchers to ensure your code is sending the right parameters and provide
- different responses based on the request body contents. ``responses`` provides
- matchers for JSON and URL-encoded request bodies.
- URL-encoded data
- """"""""""""""""
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- @responses.activate
- def test_calc_api():
- responses.post(
- url="http://calc.com/sum",
- body="4",
- match=[matchers.urlencoded_params_matcher({"left": "1", "right": "3"})],
- )
- requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
- JSON encoded data
- """""""""""""""""
- Matching JSON encoded data can be done with ``matchers.json_params_matcher()``.
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- @responses.activate
- def test_calc_api():
- responses.post(
- url="http://example.com/",
- body="one",
- match=[
- matchers.json_params_matcher({"page": {"name": "first", "type": "json"}})
- ],
- )
- resp = requests.request(
- "POST",
- "http://example.com/",
- headers={"Content-Type": "application/json"},
- json={"page": {"name": "first", "type": "json"}},
- )
- Query Parameters Matcher
- ^^^^^^^^^^^^^^^^^^^^^^^^
- Query Parameters as a Dictionary
- """"""""""""""""""""""""""""""""
- You can use the ``matchers.query_param_matcher`` function to match
- against the ``params`` request parameter. Just use the same dictionary as you
- will use in ``params`` argument in ``request``.
- Note, do not use query parameters as part of the URL. Avoid using ``match_querystring``
- deprecated argument.
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- @responses.activate
- def test_calc_api():
- url = "http://example.com/test"
- params = {"hello": "world", "I am": "a big test"}
- responses.get(
- url=url,
- body="test",
- match=[matchers.query_param_matcher(params)],
- )
- resp = requests.get(url, params=params)
- constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
- assert resp.url == constructed_url
- assert resp.request.url == constructed_url
- assert resp.request.params == params
- By default, matcher will validate that all parameters match strictly.
- To validate that only parameters specified in the matcher are present in original request
- use ``strict_match=False``.
- Query Parameters as a String
- """"""""""""""""""""""""""""
- As alternative, you can use query string value in ``matchers.query_string_matcher`` to match
- query parameters in your request
- .. code-block:: python
- import requests
- import responses
- from responses import matchers
- @responses.activate
- def my_func():
- responses.get(
- "https://httpbin.org/get",
- match=[matchers.query_string_matcher("didi=pro&test=1")],
- )
- resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"})
- my_func()
- Request Keyword Arguments Matcher
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match
- against the request kwargs.
- Only following arguments are supported: ``timeout``, ``verify``, ``proxies``, ``stream``, ``cert``.
- Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated.
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
- req_kwargs = {
- "stream": True,
- "verify": False,
- }
- rsps.add(
- "GET",
- "http://111.com",
- match=[matchers.request_kwargs_matcher(req_kwargs)],
- )
- requests.get("http://111.com", stream=True)
- # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False}
- Request multipart/form-data Data Validation
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- To validate request body and headers for ``multipart/form-data`` data you can use
- ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared
- to the request:
- .. code-block:: python
- import requests
- import responses
- from responses.matchers import multipart_matcher
- @responses.activate
- def my_func():
- req_data = {"some": "other", "data": "fields"}
- req_files = {"file_name": b"Old World!"}
- responses.post(
- url="http://httpbin.org/post",
- match=[multipart_matcher(req_files, data=req_data)],
- )
- resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"})
- my_func()
- # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.
- Request Fragment Identifier Validation
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``.
- The matcher takes fragment string (everything after ``#`` sign) as input for comparison:
- .. code-block:: python
- import requests
- import responses
- from responses.matchers import fragment_identifier_matcher
- @responses.activate
- def run():
- url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
- responses.get(
- url,
- match=[fragment_identifier_matcher("test=1&foo=bar")],
- body=b"test",
- )
- # two requests to check reversed order of fragment identifier
- resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
- resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
- run()
- Request Headers Validation
- ^^^^^^^^^^^^^^^^^^^^^^^^^^
- When adding responses you can specify matchers to ensure that your code is
- sending the right headers and provide different responses based on the request
- headers.
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- @responses.activate
- def test_content_type():
- responses.get(
- url="http://example.com/",
- body="hello world",
- match=[matchers.header_matcher({"Accept": "text/plain"})],
- )
- responses.get(
- url="http://example.com/",
- json={"content": "hello world"},
- match=[matchers.header_matcher({"Accept": "application/json"})],
- )
- # request in reverse order to how they were added!
- resp = requests.get("http://example.com/", headers={"Accept": "application/json"})
- assert resp.json() == {"content": "hello world"}
- resp = requests.get("http://example.com/", headers={"Accept": "text/plain"})
- assert resp.text == "hello world"
- Because ``requests`` will send several standard headers in addition to what was
- specified by your code, request headers that are additional to the ones
- passed to the matcher are ignored by default. You can change this behaviour by
- passing ``strict_match=True`` to the matcher to ensure that only the headers
- that you're expecting are sent and no others. Note that you will probably have
- to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't
- include any additional headers.
- .. code-block:: python
- import responses
- import requests
- from responses import matchers
- @responses.activate
- def test_content_type():
- responses.get(
- url="http://example.com/",
- body="hello world",
- match=[matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)],
- )
- # this will fail because requests adds its own headers
- with pytest.raises(ConnectionError):
- requests.get("http://example.com/", headers={"Accept": "text/plain"})
- # a prepared request where you overwrite the headers before sending will work
- session = requests.Session()
- prepped = session.prepare_request(
- requests.Request(
- method="GET",
- url="http://example.com/",
- )
- )
- prepped.headers = {"Accept": "text/plain"}
- resp = session.send(prepped)
- assert resp.text == "hello world"
- Creating Custom Matcher
- ^^^^^^^^^^^^^^^^^^^^^^^
- If your application requires other encodings or different data validation you can build
- your own matcher that returns ``Tuple[matches: bool, reason: str]``.
- Where boolean represents ``True`` or ``False`` if the request parameters match and
- the string is a reason in case of match failure. Your matcher can
- expect a ``PreparedRequest`` parameter to be provided by ``responses``.
- Note, ``PreparedRequest`` is customized and has additional attributes ``params`` and ``req_kwargs``.
- Response Registry
- ---------------------------
- Default Registry
- ^^^^^^^^^^^^^^^^
- By default, ``responses`` will search all registered ``Response`` objects and
- return a match. If only one ``Response`` is registered, the registry is kept unchanged.
- However, if multiple matches are found for the same request, then first match is returned and
- removed from registry.
- Ordered Registry
- ^^^^^^^^^^^^^^^^
- In some scenarios it is important to preserve the order of the requests and responses.
- You can use ``registries.OrderedRegistry`` to force all ``Response`` objects to be dependent
- on the insertion order and invocation index.
- In following example we add multiple ``Response`` objects that target the same URL. However,
- you can see, that status code will depend on the invocation order.
- .. code-block:: python
- import requests
- import responses
- from responses.registries import OrderedRegistry
- @responses.activate(registry=OrderedRegistry)
- def test_invocation_index():
- responses.get(
- "http://twitter.com/api/1/foobar",
- json={"msg": "not found"},
- status=404,
- )
- responses.get(
- "http://twitter.com/api/1/foobar",
- json={"msg": "OK"},
- status=200,
- )
- responses.get(
- "http://twitter.com/api/1/foobar",
- json={"msg": "OK"},
- status=200,
- )
- responses.get(
- "http://twitter.com/api/1/foobar",
- json={"msg": "not found"},
- status=404,
- )
- resp = requests.get("http://twitter.com/api/1/foobar")
- assert resp.status_code == 404
- resp = requests.get("http://twitter.com/api/1/foobar")
- assert resp.status_code == 200
- resp = requests.get("http://twitter.com/api/1/foobar")
- assert resp.status_code == 200
- resp = requests.get("http://twitter.com/api/1/foobar")
- assert resp.status_code == 404
- Custom Registry
- ^^^^^^^^^^^^^^^
- Built-in ``registries`` are suitable for most of use cases, but to handle special conditions, you can
- implement custom registry which must follow interface of ``registries.FirstMatchRegistry``.
- Redefining the ``find`` method will allow you to create custom search logic and return
- appropriate ``Response``
- Example that shows how to set custom registry
- .. code-block:: python
- import responses
- from responses import registries
- class CustomRegistry(registries.FirstMatchRegistry):
- pass
- print("Before tests:", responses.mock.get_registry())
- """ Before tests: <responses.registries.FirstMatchRegistry object> """
- # using function decorator
- @responses.activate(registry=CustomRegistry)
- def run():
- print("Within test:", responses.mock.get_registry())
- """ Within test: <__main__.CustomRegistry object> """
- run()
- print("After test:", responses.mock.get_registry())
- """ After test: <responses.registries.FirstMatchRegistry object> """
- # using context manager
- with responses.RequestsMock(registry=CustomRegistry) as rsps:
- print("In context manager:", rsps.get_registry())
- """ In context manager: <__main__.CustomRegistry object> """
- print("After exit from context manager:", responses.mock.get_registry())
- """
- After exit from context manager: <responses.registries.FirstMatchRegistry object>
- """
- 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",
- )
- Integration with unit test frameworks
- -------------------------------------
- Responses as a ``pytest`` fixture
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Use the pytest-responses package to export ``responses`` as a pytest fixture.
- ``pip install pytest-responses``
- You can then access it in a pytest script using:
- .. code-block:: python
- import pytest_responses
- def test_api(responses):
- 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
- Add default responses for each test
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- When run with ``unittest`` tests, this can be used to set up some
- generic class-level responses, that may be complemented by each test.
- Similar interface could be applied in ``pytest`` framework.
- .. code-block:: python
- class TestMyApi(unittest.TestCase):
- def setUp(self):
- responses.get("https://example.com", body="within setup")
- # here go other self.responses.add(...)
- @responses.activate
- def test_my_func(self):
- responses.get(
- "https://httpbin.org/get",
- match=[matchers.query_param_matcher({"test": "1", "didi": "pro"})],
- body="within test",
- )
- resp = requests.get("https://example.com")
- resp2 = requests.get(
- "https://httpbin.org/get", params={"test": "1", "didi": "pro"}
- )
- print(resp.text)
- # >>> within setup
- print(resp2.text)
- # >>> within test
- RequestMock methods: start, stop, reset
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- ``responses`` has ``start``, ``stop``, ``reset`` methods very analogous to
- `unittest.mock.patch <https://docs.python.org/3/library/unittest.mock.html#patch-methods-start-and-stop>`_.
- These make it simpler to do requests mocking in ``setup`` methods or where
- you want to do multiple patches without nesting decorators or with statements.
- .. code-block:: python
- class TestUnitTestPatchSetup:
- def setup(self):
- """Creates ``RequestsMock`` instance and starts it."""
- self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True)
- self.r_mock.start()
- # optionally some default responses could be registered
- self.r_mock.get("https://example.com", status=505)
- self.r_mock.put("https://example.com", status=506)
- def teardown(self):
- """Stops and resets RequestsMock instance.
- If ``assert_all_requests_are_fired`` is set to ``True``, will raise an error
- if some requests were not processed.
- """
- self.r_mock.stop()
- self.r_mock.reset()
- def test_function(self):
- resp = requests.get("https://example.com")
- assert resp.status_code == 505
- resp = requests.put("https://example.com")
- assert resp.status_code == 506
- 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",
- )
- Assert Request Call Count
- -------------------------
- Assert based on ``Response`` object
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Each ``Response`` object has ``call_count`` attribute that could be inspected
- to check how many times each request was matched.
- .. code-block:: python
- @responses.activate
- def test_call_count_with_matcher():
- rsp = responses.get(
- "http://www.example.com",
- match=(matchers.query_param_matcher({}),),
- )
- rsp2 = responses.get(
- "http://www.example.com",
- match=(matchers.query_param_matcher({"hello": "world"}),),
- status=777,
- )
- requests.get("http://www.example.com")
- resp1 = requests.get("http://www.example.com")
- requests.get("http://www.example.com?hello=world")
- resp2 = requests.get("http://www.example.com?hello=world")
- assert resp1.status_code == 200
- assert resp2.status_code == 777
- assert rsp.call_count == 2
- assert rsp2.call_count == 2
- Assert based on the exact URL
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Assert that the request was called exactly n times.
- .. code-block:: python
- import responses
- import requests
- @responses.activate
- def test_assert_call_count():
- responses.get("http://example.com")
- requests.get("http://example.com")
- assert responses.assert_call_count("http://example.com", 1) is True
- requests.get("http://example.com")
- with pytest.raises(AssertionError) as excinfo:
- responses.assert_call_count("http://example.com", 1)
- assert (
- "Expected URL 'http://example.com' to be called 1 times. Called 2 times."
- in str(excinfo.value)
- )
- @responses.activate
- def test_assert_call_count_always_match_qs():
- responses.get("http://www.example.com")
- requests.get("http://www.example.com")
- requests.get("http://www.example.com?hello=world")
- # One call on each url, querystring is matched by default
- responses.assert_call_count("http://www.example.com", 1) is True
- responses.assert_call_count("http://www.example.com?hello=world", 1) is True
- Assert Request Calls data
- -------------------------
- ``Request`` object has ``calls`` list which elements correspond to ``Call`` objects
- in the global list of ``Registry``. This can be useful when the order of requests is not
- guaranteed, but you need to check their correctness, for example in multithreaded
- applications.
- .. code-block:: python
- import concurrent.futures
- import responses
- import requests
- @responses.activate
- def test_assert_calls_on_resp():
- rsp1 = responses.patch("http://www.foo.bar/1/", status=200)
- rsp2 = responses.patch("http://www.foo.bar/2/", status=400)
- rsp3 = responses.patch("http://www.foo.bar/3/", status=200)
- def update_user(uid, is_active):
- url = f"http://www.foo.bar/{uid}/"
- response = requests.patch(url, json={"is_active": is_active})
- return response
- with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
- future_to_uid = {
- executor.submit(update_user, uid, is_active): uid
- for (uid, is_active) in [("3", True), ("2", True), ("1", False)]
- }
- for future in concurrent.futures.as_completed(future_to_uid):
- uid = future_to_uid[future]
- response = future.result()
- print(f"{uid} updated with {response.status_code} status code")
- assert len(responses.calls) == 3 # total calls count
- assert rsp1.call_count == 1
- assert rsp1.calls[0] in responses.calls
- assert rsp1.calls[0].response.status_code == 200
- assert json.loads(rsp1.calls[0].request.body) == {"is_active": False}
- assert rsp2.call_count == 1
- assert rsp2.calls[0] in responses.calls
- assert rsp2.calls[0].response.status_code == 400
- assert json.loads(rsp2.calls[0].request.body) == {"is_active": True}
- assert rsp3.call_count == 1
- assert rsp3.calls[0] in responses.calls
- assert rsp3.calls[0].response.status_code == 200
- assert json.loads(rsp3.calls[0].request.body) == {"is_active": True}
- 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.get("http://twitter.com/api/1/foobar", status=500)
- 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
- URL Redirection
- ---------------
- In the following example you can see how to create a redirection chain and add custom exception that will be raised
- in the execution chain and contain the history of redirects.
- .. code-block::
- A -> 301 redirect -> B
- B -> 301 redirect -> C
- C -> connection issue
- .. code-block:: python
- import pytest
- import requests
- import responses
- @responses.activate
- def test_redirect():
- # create multiple Response objects where first two contain redirect headers
- rsp1 = responses.Response(
- responses.GET,
- "http://example.com/1",
- status=301,
- headers={"Location": "http://example.com/2"},
- )
- rsp2 = responses.Response(
- responses.GET,
- "http://example.com/2",
- status=301,
- headers={"Location": "http://example.com/3"},
- )
- rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200)
- # register above generated Responses in ``response`` module
- responses.add(rsp1)
- responses.add(rsp2)
- responses.add(rsp3)
- # do the first request in order to generate genuine ``requests`` response
- # this object will contain genuine attributes of the response, like ``history``
- rsp = requests.get("http://example.com/1")
- responses.calls.reset()
- # customize exception with ``response`` attribute
- my_error = requests.ConnectionError("custom error")
- my_error.response = rsp
- # update body of the 3rd response with Exception, this will be raised during execution
- rsp3.body = my_error
- with pytest.raises(requests.ConnectionError) as exc_info:
- requests.get("http://example.com/1")
- assert exc_info.value.args[0] == "custom error"
- assert rsp1.url in exc_info.value.response.history[0].url
- assert rsp2.url in exc_info.value.response.history[1].url
- Validate ``Retry`` mechanism
- ----------------------------
- If you are using the ``Retry`` features of ``urllib3`` and want to cover scenarios that test your retry limits, you can test those scenarios with ``responses`` as well. The best approach will be to use an `Ordered Registry`_
- .. code-block:: python
- import requests
- import responses
- from responses import registries
- from urllib3.util import Retry
- @responses.activate(registry=registries.OrderedRegistry)
- def test_max_retries():
- url = "https://example.com"
- rsp1 = responses.get(url, body="Error", status=500)
- rsp2 = responses.get(url, body="Error", status=500)
- rsp3 = responses.get(url, body="Error", status=500)
- rsp4 = responses.get(url, body="OK", status=200)
- session = requests.Session()
- adapter = requests.adapters.HTTPAdapter(
- max_retries=Retry(
- total=4,
- backoff_factor=0.1,
- status_forcelist=[500],
- method_whitelist=["GET", "POST", "PATCH"],
- )
- )
- session.mount("https://", adapter)
- resp = session.get(url)
- assert resp.status_code == 200
- assert rsp1.call_count == 1
- assert rsp2.call_count == 1
- assert rsp3.call_count == 1
- assert rsp4.call_count == 1
- 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 through real requests
- -----------------------------
- In some cases you may wish to allow for certain requests to pass through responses
- and hit a real server. This can be done with the ``add_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.
- Pass through endpoints can be configured with regex patterns if you
- need to allow an entire domain or path subtree to send requests:
- .. code-block:: python
- responses.add_passthru(re.compile("https://percy.io/\\w+"))
- Lastly, you can use the ``passthrough`` argument of the ``Response`` object
- to force a response to behave as a pass through.
- .. code-block:: python
- # Enable passthrough for a single response
- response = Response(
- responses.GET,
- "http://example.com",
- body="not used",
- passthrough=True,
- )
- responses.add(response)
- # Use PassthroughResponse
- response = PassthroughResponse(responses.GET, "http://example.com")
- responses.add(response)
- Viewing/Modifying registered responses
- --------------------------------------
- Registered responses are available as a public method of the RequestMock
- instance. It is sometimes useful for debugging purposes to view the stack of
- registered responses which can be accessed via ``responses.registered()``.
- 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.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}
- The ``upsert`` function allows a previously registered ``response`` to be
- changed like ``replace``. If the response is registered, the ``upsert`` function
- will registered it like ``add``.
- ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
- matched responses from the registered list.
- Finally, ``reset`` will reset all registered responses.
- Coroutines and Multithreading
- -----------------------------
- ``responses`` supports both Coroutines and Multithreading out of the box.
- Note, ``responses`` locks threading on ``RequestMock`` object allowing only
- single thread to access it.
- .. code-block:: python
- async def test_async_calls():
- @responses.activate
- async def run():
- 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 responses.calls[0].request.url == "http://twitter.com/api/1/foobar"
- await run()
- BETA Features
- -------------
- Below you can find a list of BETA features. Although we will try to keep the API backwards compatible
- with released version, we reserve the right to change these APIs before they are considered stable. Please share your feedback via
- `GitHub Issues <https://github.com/getsentry/responses/issues>`_.
- Record Responses to files
- ^^^^^^^^^^^^^^^^^^^^^^^^^
- You can perform real requests to the server and ``responses`` will automatically record the output to the
- file. Recorded data is stored in `YAML <https://yaml.org>`_ format.
- Apply ``@responses._recorder.record(file_path="out.yaml")`` decorator to any function where you perform
- requests to record responses to ``out.yaml`` file.
- Following code
- .. code-block:: python
- import requests
- from responses import _recorder
- def another():
- rsp = requests.get("https://httpstat.us/500")
- rsp = requests.get("https://httpstat.us/202")
- @_recorder.record(file_path="out.yaml")
- def test_recorder():
- rsp = requests.get("https://httpstat.us/404")
- rsp = requests.get("https://httpbin.org/status/wrong")
- another()
- will produce next output:
- .. code-block:: yaml
- responses:
- - response:
- auto_calculate_content_length: false
- body: 404 Not Found
- content_type: text/plain
- method: GET
- status: 404
- url: https://httpstat.us/404
- - response:
- auto_calculate_content_length: false
- body: Invalid status code
- content_type: text/plain
- method: GET
- status: 400
- url: https://httpbin.org/status/wrong
- - response:
- auto_calculate_content_length: false
- body: 500 Internal Server Error
- content_type: text/plain
- method: GET
- status: 500
- url: https://httpstat.us/500
- - response:
- auto_calculate_content_length: false
- body: 202 Accepted
- content_type: text/plain
- method: GET
- status: 202
- url: https://httpstat.us/202
- If you are in the REPL, you can also activete the recorder for all following responses:
- .. code-block:: python
- import requests
- from responses import _recorder
- _recorder.recorder.start()
- requests.get("https://httpstat.us/500")
- _recorder.recorder.dump_to_file("out.yaml")
- # you can stop or reset the recorder
- _recorder.recorder.stop()
- _recorder.recorder.reset()
- Replay responses (populate registry) from files
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- You can populate your active registry from a ``yaml`` file with recorded responses.
- (See `Record Responses to files`_ to understand how to obtain a file).
- To do that you need to execute ``responses._add_from_file(file_path="out.yaml")`` within
- an activated decorator or a context manager.
- The following code example registers a ``patch`` response, then all responses present in
- ``out.yaml`` file and a ``post`` response at the end.
- .. code-block:: python
- import responses
- @responses.activate
- def run():
- responses.patch("http://httpbin.org")
- responses._add_from_file(file_path="out.yaml")
- responses.post("http://httpbin.org/form")
- run()
- Contributing
- ------------
- Environment Configuration
- ^^^^^^^^^^^^^^^^^^^^^^^^^
- 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
- Tests and Code Quality Validation
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- The easiest way to validate your code is to run tests via ``tox``.
- Current ``tox`` configuration runs the same checks that are used in
- GitHub Actions CI/CD pipeline.
- Please execute the following command line from the project root to validate
- your code against:
- * Unit tests in all Python versions that are supported by this project
- * Type validation via ``mypy``
- * All ``pre-commit`` hooks
- .. code-block:: shell
- tox
- Alternatively, you can always run a single test. See documentation below.
- Unit tests
- """"""""""
- Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for
- testing. You can run all tests by:
- .. code-block:: shell
- tox -e py37
- tox -e py310
- OR manually activate required version of Python and run
- .. code-block:: shell
- pytest
- And run a single test by:
- .. code-block:: shell
- pytest -k '<test_function_name>'
- Type Validation
- """""""""""""""
- To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter:
- .. code-block:: shell
- tox -e mypy
- OR
- .. code-block:: shell
- mypy --config-file=./mypy.ini -p responses
- Code Quality and Style
- """"""""""""""""""""""
- To check code style and reformat it run:
- .. code-block:: shell
- tox -e precom
- OR
- .. code-block:: shell
- pre-commit run --all-files
|