12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502 |
- 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.
- 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
- 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()
- 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
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- .. code-block:: python
- @pytest.fixture
- def mocked_responses():
- with responses.RequestsMock() as rsps:
- yield rsps
- def test_api(mocked_responses):
- mocked_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()
- 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
|