Repository: getsentry/responses
Pull request #535 (2022-04-05 08:15:28)

mbeliaev/resp types
add type annotations to `Response` objects.

until do a full type migration we have to keep redundant definition in stub file

codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#535](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (3565faa) into [master](https://codecov.io/gh/getsentry/responses/commit/bf30737282793b9290c1f064c85db5769d2d4ccf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (bf30737) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master      #535   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            7         7           
  Lines         2378      2382    +4     
=========================================
+ Hits          2378      2382    +4     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/535/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [bf30737...3565faa](https://app.codecov.io/gh/getsentry/responses/compare/535?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).


Patch:
diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml
new file mode 100644
index 0000000..f6afabd
--- /dev/null
+++ b/.github/workflows/enforce-license-compliance.yml
@@ -0,0 +1,16 @@
+name: Enforce License Compliance
+
+on:
+  push:
+    branches: [master, release/*]
+  pull_request:
+    branches: [master]
+
+jobs:
+  enforce-license-compliance:
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Enforce License Compliance'
+        uses: getsentry/action-enforce-license-compliance@main
+        with:
+          fossa_api_key: ${{ secrets.FOSSA_API_KEY }}
diff --git a/CHANGES b/CHANGES
index e916719..fbf8ddb 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2,9 +2,14 @@
 ------
 
 * Add `threading.Lock()` to allow `responses` working with `threading` module.
+* Add `urllib3` `Retry` mechanism. See #135
 * Removed internal `_cookies_from_headers` function
 * Now `add`, `upsert`, `replace` methods return registered response.
   `remove` method returns list of removed responses.
+* Added null value support in `urlencoded_params_matcher` via `allow_blank` keyword argument
+* Added strict version of decorator. Now you can apply `@responses.activate(assert_all_requests_are_fired=True)`
+  to your function to validate that all requests were executed in the wrapped function. See #183
+
 
 0.20.0
 ------
diff --git a/MANIFEST.in b/MANIFEST.in
index 2934690..2f743f5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,4 @@
 include README.rst CHANGES LICENSE
 recursive-include responses *.py
-include **/*.pyi
 include tox.ini
 global-exclude *~
diff --git a/README.rst b/README.rst
index 94b960f..014dd6f 100644
--- a/README.rst
+++ b/README.rst
@@ -737,8 +737,11 @@ Responses as a context manager
         resp = requests.get('http://twitter.com/api/1/foobar')
         resp.status_code == 404
 
-Responses as a pytest fixture
------------------------------
+Integration with unit test frameworks
+-------------------------------------
+
+Responses as a ``pytest`` fixture
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. code-block:: python
 
@@ -755,11 +758,12 @@ Responses as a pytest fixture
         resp = requests.get('http://twitter.com/api/1/foobar')
         assert resp.status_code == 200
 
-Responses inside a unittest setUp()
------------------------------------
+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
+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
 
@@ -784,6 +788,43 @@ generic class-level responses, that may be complemented by each test
             # >>> 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
 --------------------------------
 
@@ -956,6 +997,44 @@ in the execution chain and contain the history of redirects.
         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
+
+    @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
 ---------------------------------------
diff --git a/responses/__init__.py b/responses/__init__.py
index 460d24e..b0e8c2a 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -13,12 +13,16 @@ from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
 from typing import Dict
+from typing import Iterable
 from typing import Iterator
+from typing import Mapping
 from typing import Optional
+from typing import Tuple
 from typing import Union
 from warnings import warn
 
 from requests.adapters import HTTPAdapter
+from requests.adapters import MaxRetryError
 from requests.exceptions import ConnectionError
 
 from responses.matchers import json_params_matcher as _json_params_matcher
@@ -54,6 +58,7 @@ if TYPE_CHECKING:  # pragma: no cover
 
 # Block of type annotations
 _Body = Union[str, BaseException, "Response", BufferedReader, bytes]
+_MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]]
 
 Call = namedtuple("Call", ["request", "response"])
 _real_send = HTTPAdapter.send
@@ -75,12 +80,14 @@ class FalseBool:
     __nonzero__ = __bool__
 
 
-def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[..., Any]:
+def urlencoded_params_matcher(
+    params: Optional[Dict[str, str]], **kwargs: Optional[Dict[str, str]]
+) -> Callable[..., Any]:
     warn(
         "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'",
         DeprecationWarning,
     )
-    return _urlencoded_params_matcher(params)
+    return _urlencoded_params_matcher(params, **kwargs)
 
 
 def json_params_matcher(params: Optional[Dict[str, Any]]) -> Callable[..., Any]:
@@ -132,7 +139,11 @@ def _clean_unicode(url: str) -> str:
 
 
 def get_wrapped(
-    func: Callable[..., Any], responses: "RequestsMock", registry: Optional[Any] = None
+    func: Callable[..., Any],
+    responses: "RequestsMock",
+    *,
+    registry: Optional[Any] = None,
+    assert_all_requests_are_fired: Optional[bool] = None,
 ) -> Callable[..., Any]:
     """Wrap provided function inside ``responses`` context manager.
 
@@ -147,6 +158,8 @@ def get_wrapped(
         Mock object that is used as context manager.
     registry : FirstMatchRegistry, optional
         Custom registry that should be applied. See ``responses.registries``
+    assert_all_requests_are_fired : bool
+        Raise an error if not all registered responses were executed.
 
     Returns
     -------
@@ -157,18 +170,29 @@ def get_wrapped(
     if registry is not None:
         responses._set_registry(registry)
 
+    assert_mock = std_mock.patch.object(
+        target=responses,
+        attribute="assert_all_requests_are_fired",
+        new=assert_all_requests_are_fired,
+    )
+
     if inspect.iscoroutinefunction(func):
         # set asynchronous wrapper if requestor function is asynchronous
         @wraps(func)
         async def wrapper(*args, **kwargs):
-            with responses:
+
+            with assert_mock, responses:
                 return await func(*args, **kwargs)
 
     else:
 
         @wraps(func)
         def wrapper(*args, **kwargs):
-            with responses:
+
+            with assert_mock, responses:
+                # set 'assert_all_requests_are_fired' temporarily for a single run.
+                # Mock automatically unsets to avoid leakage to another decorated
+                # function since we still apply the value on 'responses.mock' object
                 return func(*args, **kwargs)
 
     return wrapper
@@ -275,7 +299,7 @@ def _handle_body(
 
     data = BytesIO(body)
 
-    def is_closed():
+    def is_closed() -> bool:
         """
         Real Response uses HTTPResponse as body object.
         Thus, when method is_closed is called first to check if there is any more
@@ -305,23 +329,29 @@ def _handle_body(
 
 
 class BaseResponse(object):
-    passthrough = False
-    content_type = None
-    headers = None
-    stream = False
+    passthrough: bool = False
+    content_type: Optional[str] = None
+    headers: Optional[Mapping[str, str]] = None
+    stream: bool = False
 
-    def __init__(self, method, url, match_querystring=None, match=()):
-        self.method = method
+    def __init__(
+        self,
+        method: str,
+        url: "Union[Pattern[str], str]",
+        match_querystring: Union[bool, object] = None,
+        match: "_MatcherIterable" = (),
+    ) -> None:
+        self.method: str = method
         # ensure the url has a default path set if the url is a string
-        self.url = _ensure_url_default_path(url)
+        self.url: "Union[Pattern[str], str]" = _ensure_url_default_path(url)
 
         if self._should_match_querystring(match_querystring):
             match = tuple(match) + (_query_string_matcher(urlsplit(self.url).query),)
 
-        self.match = match
-        self.call_count = 0
+        self.match: "_MatcherIterable" = match
+        self.call_count: int = 0
 
-    def __eq__(self, other):
+    def __eq__(self, other: Any) -> bool:
         if not isinstance(other, BaseResponse):
             return False
 
@@ -336,10 +366,12 @@ class BaseResponse(object):
 
         return self_url == other_url
 
-    def __ne__(self, other):
+    def __ne__(self, other: Any) -> bool:
         return not self.__eq__(other)
 
-    def _should_match_querystring(self, match_querystring_argument):
+    def _should_match_querystring(
+        self, match_querystring_argument: Union[bool, object]
+    ) -> Union[bool, object]:
         if isinstance(self.url, Pattern):
             # the old default from <= 0.9.0
             return False
@@ -358,7 +390,7 @@ class BaseResponse(object):
 
         return bool(urlsplit(self.url).query)
 
-    def _url_matches(self, url, other):
+    def _url_matches(self, url: "Union[Pattern[str], str]", other: str) -> bool:
         if isinstance(url, str):
             if _has_unicode(url):
                 url = _clean_unicode(url)
@@ -372,7 +404,9 @@ class BaseResponse(object):
             return False
 
     @staticmethod
-    def _req_attr_matches(match, request):
+    def _req_attr_matches(
+        match: "_MatcherIterable", request: "PreparedRequest"
+    ) -> Tuple[bool, str]:
         for matcher in match:
             valid, reason = matcher(request)
             if not valid:
@@ -380,7 +414,7 @@ class BaseResponse(object):
 
         return True, ""
 
-    def get_headers(self):
+    def get_headers(self) -> HTTPHeaderDict:
         headers = HTTPHeaderDict()  # Duplicate headers are legal
         if self.content_type is not None:
             headers["Content-Type"] = self.content_type
@@ -388,10 +422,10 @@ class BaseResponse(object):
             headers.extend(self.headers)
         return headers
 
-    def get_response(self, request):
+    def get_response(self, request: "PreparedRequest") -> None:
         raise NotImplementedError
 
-    def matches(self, request):
+    def matches(self, request: "PreparedRequest") -> Tuple[bool, str]:
         if request.method != self.method:
             return False, "Method does not match"
 
@@ -408,17 +442,17 @@ class BaseResponse(object):
 class Response(BaseResponse):
     def __init__(
         self,
-        method,
-        url,
-        body="",
-        json=None,
-        status=200,
-        headers=None,
-        stream=None,
-        content_type=_UNSET,
-        auto_calculate_content_length=False,
+        method: str,
+        url: "Union[Pattern[str], str]",
+        body: _Body = "",
+        json: Optional[Any] = None,
+        status: int = 200,
+        headers: Optional[Mapping[str, str]] = None,
+        stream: bool = None,
+        content_type: Optional[str] = _UNSET,
+        auto_calculate_content_length: bool = False,
         **kwargs,
-    ):
+    ) -> None:
         # if we were passed a `json` argument,
         # override the body and content_type
         if json is not None:
@@ -433,9 +467,9 @@ class Response(BaseResponse):
             else:
                 content_type = "text/plain"
 
-        self.body = body
-        self.status = status
-        self.headers = headers
+        self.body: _Body = body
+        self.status: int = status
+        self.headers: Optional[Mapping[str, str]] = headers
 
         if stream is not None:
             warn(
@@ -443,12 +477,12 @@ class Response(BaseResponse):
                 DeprecationWarning,
             )
 
-        self.stream = stream
-        self.content_type = content_type
-        self.auto_calculate_content_length = auto_calculate_content_length
+        self.stream: bool = stream
+        self.content_type: Optional[str] = content_type
+        self.auto_calculate_content_length: bool = auto_calculate_content_length
         super().__init__(method, url, **kwargs)
 
-    def get_response(self, request):
+    def get_response(self, request: "PreparedRequest") -> HTTPResponse:
         if self.body and isinstance(self.body, Exception):
             raise self.body
 
@@ -473,7 +507,7 @@ class Response(BaseResponse):
             preload_content=False,
         )
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return (
             "<Response(url='{url}' status={status} "
             "content_type='{content_type}' headers='{headers}')>".format(
@@ -487,8 +521,14 @@ class Response(BaseResponse):
 
 class CallbackResponse(BaseResponse):
     def __init__(
-        self, method, url, callback, stream=None, content_type="text/plain", **kwargs
-    ):
+        self,
+        method: str,
+        url: "Union[Pattern[str], str]",
+        callback: Callable[[Any], Any],
+        stream: bool = None,
+        content_type: Optional[str] = "text/plain",
+        **kwargs,
+    ) -> None:
         self.callback = callback
 
         if stream is not None:
@@ -496,11 +536,11 @@ class CallbackResponse(BaseResponse):
                 "stream argument is deprecated. Use stream parameter in request directly",
                 DeprecationWarning,
             )
-        self.stream = stream
-        self.content_type = content_type
+        self.stream: bool = stream
+        self.content_type: Optional[str] = content_type
         super().__init__(method, url, **kwargs)
 
-    def get_response(self, request):
+    def get_response(self, request: "PreparedRequest") -> HTTPResponse:
         headers = self.get_headers()
 
         result = self.callback(request)
@@ -538,7 +578,7 @@ class CallbackResponse(BaseResponse):
 
 
 class PassthroughResponse(BaseResponse):
-    passthrough = True
+    passthrough: bool = True
 
 
 class OriginalResponseShim(object):
@@ -790,12 +830,19 @@ class RequestsMock(object):
         self.reset()
         return success
 
-    def activate(self, func=None, registry=None):
+    def activate(
+        self, func=None, *, registry=None, assert_all_requests_are_fired=False
+    ):
         if func is not None:
             return get_wrapped(func, self)
 
         def deco_activate(function):
-            return get_wrapped(function, self, registry)
+            return get_wrapped(
+                function,
+                self,
+                registry=registry,
+                assert_all_requests_are_fired=assert_all_requests_are_fired,
+            )
 
         return deco_activate
 
@@ -820,7 +867,7 @@ class RequestsMock(object):
             params[key] = values
         return params
 
-    def _on_request(self, adapter, request, **kwargs):
+    def _on_request(self, adapter, request, *, retries=None, **kwargs):
         # add attributes params and req_kwargs to 'request' object for further match comparison
         # original request object does not have these attributes
         request.params = self._parse_request_params(request.path_url)
@@ -878,6 +925,22 @@ class RequestsMock(object):
         response = resp_callback(response) if resp_callback else response
         match.call_count += 1
         self._calls.add(request, response)
+
+        retries = retries or adapter.max_retries
+        # first validate that current request is eligible to be retried.
+        # See ``requests.packages.urllib3.util.retry.Retry`` documentation.
+        if retries.is_retry(
+            method=response.request.method, status_code=response.status_code
+        ):
+            try:
+                retries = retries.increment(
+                    method=response.request.method, url=response.url, response=response
+                )
+                return self._on_request(adapter, request, retries=retries, **kwargs)
+            except MaxRetryError:
+                if retries.raise_on_status:
+                    raise
+                return response
         return response
 
     def start(self):
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 89a2bfa..25ac4f7 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -294,8 +294,10 @@ class _Activate(Protocol):
     # use this overload for scenario when 'responses.activate' is used
 
     @overload
-    def __call__(self, registry: Type[Any] = ...) -> Callable[["_F"], "_F"]: ...
-    # use this overload for scenario when 'responses.activate(registry=)' is used
+    def __call__(
+        self, registry: Type[Any] = ..., assert_all_requests_are_fired: bool = ...
+    ) -> Callable[["_F"], "_F"]: ...
+    # use this overload for scenario when 'responses.activate(registry=, assert_all_requests_are_fired=True)' is used
 
 activate: _Activate
 add: _Add
diff --git a/responses/matchers.py b/responses/matchers.py
index 4f8ebdd..6800947 100644
--- a/responses/matchers.py
+++ b/responses/matchers.py
@@ -53,7 +53,9 @@ def _create_key_val_str(input_dict: Union[Dict[Any, Any], Any]) -> str:
     return key_val_str
 
 
-def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[..., Any]:
+def urlencoded_params_matcher(
+    params: Optional[Dict[str, str]], *, allow_blank: bool = False
+) -> Callable[..., Any]:
     """
     Matches URL encoded data
 
@@ -64,7 +66,11 @@ def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[...,
     def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         request_body = request.body
-        qsl_body = dict(parse_qsl(request_body)) if request_body else {}  # type: ignore[type-var]
+        qsl_body = (
+            dict(parse_qsl(request_body, keep_blank_values=allow_blank))  # type: ignore[type-var]
+            if request_body
+            else {}
+        )
         params_dict = params or {}
         valid = params is None if request_body is None else params_dict == qsl_body
         if not valid:
diff --git a/setup.py b/setup.py
index 9872714..c14b4c5 100644
--- a/setup.py
+++ b/setup.py
@@ -16,12 +16,10 @@ from setuptools.command.test import test as TestCommand
 
 setup_requires = []
 
-if "test" in sys.argv:
-    setup_requires.append("pytest")
-
 install_requires = [
     "requests>=2.0,<3.0",
     "urllib3>=1.25.10",
+    "typing_extensions; python_version < '3.8'",
 ]
 
 tests_require = [
@@ -36,13 +34,18 @@ tests_require = [
     "mypy",
 ]
 
+if "test" in sys.argv:
+    setup_requires.extend(tests_require)
+
 extras_require = {"tests": tests_require}
 
 
 class PyTest(TestCommand):
+    """Designed to be run via `python setup.py test`"""
+
     def finalize_options(self):
         TestCommand.finalize_options(self)
-        self.test_args = ["test_responses.py"]
+        self.test_args = []
         self.test_suite = True
 
     def run_tests(self):
@@ -57,7 +60,7 @@ setup(
     name="responses",
     version="0.20.0",
     author="David Cramer",
-    description=("A utility library for mocking out the `requests` Python library."),
+    description="A utility library for mocking out the `requests` Python library.",
     url="https://github.com/getsentry/responses",
     license="Apache 2.0",
     long_description=open("README.rst").read(),
@@ -70,7 +73,6 @@ setup(
     tests_require=tests_require,
     setup_requires=setup_requires,
     cmdclass={"test": PyTest},
-    package_data={"responses": ["py.typed", "__init__.pyi"]},
     include_package_data=True,
     classifiers=[
         "Intended Audience :: Developers",


Test patch:
diff --git a/responses/tests/test_matchers.py b/responses/tests/test_matchers.py
index b7c6191..20458ae 100644
--- a/responses/tests/test_matchers.py
+++ b/responses/tests/test_matchers.py
@@ -51,6 +51,24 @@ def test_request_matches_post_params():
             body="two",
             match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})],
         )
+        responses.add(
+            method=responses.POST,
+            url="http://example.com/",
+            body="three",
+            match=[
+                urlencoded_params_matcher(
+                    {"page": "", "type": "urlencoded"}, allow_blank=True
+                )
+            ],
+        )
+
+        resp = requests.request(
+            "POST",
+            "http://example.com/",
+            headers={"Content-Type": "x-www-form-urlencoded"},
+            data={"page": "", "type": "urlencoded"},
+        )
+        assert_response(resp, "three")
 
         resp = requests.request(
             "POST",
diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py
index 47479bb..4dd93f0 100644
--- a/responses/tests/test_responses.py
+++ b/responses/tests/test_responses.py
@@ -13,9 +13,11 @@ from unittest.mock import patch
 
 import pytest
 import requests
+from requests.adapters import MaxRetryError
 from requests.exceptions import ChunkedEncodingError
 from requests.exceptions import ConnectionError
 from requests.exceptions import HTTPError
+from urllib3.util.retry import Retry
 
 import responses
 from responses import BaseResponse
@@ -23,6 +25,7 @@ from responses import CallbackResponse
 from responses import PassthroughResponse
 from responses import Response
 from responses import matchers
+from responses import registries
 
 
 def assert_reset():
@@ -2059,6 +2062,71 @@ async def test_async_calls():
     assert_reset()
 
 
+class TestStrictWrapper:
+    def test_strict_wrapper(self):
+        """Test that assert_all_requests_are_fired could be applied to the decorator."""
+
+        @responses.activate(assert_all_requests_are_fired=True)
+        def run_strict():
+            responses.add(responses.GET, "https://someapi1.com/", "success")
+            responses.add(responses.GET, "https://notcalled1.com/", "success")
+            requests.get("https://someapi1.com/")
+            assert responses.mock.assert_all_requests_are_fired
+
+        @responses.activate(assert_all_requests_are_fired=False)
+        def run_not_strict():
+            responses.add(responses.GET, "https://someapi2.com/", "success")
+            responses.add(responses.GET, "https://notcalled2.com/", "success")
+            requests.get("https://someapi2.com/")
+            assert not responses.mock.assert_all_requests_are_fired
+
+        @responses.activate
+        def run_classic():
+            responses.add(responses.GET, "https://someapi3.com/", "success")
+            responses.add(responses.GET, "https://notcalled3.com/", "success")
+            requests.get("https://someapi3.com/")
+            assert not responses.mock.assert_all_requests_are_fired
+
+        # keep the order of function calls to ensure that decorator doesn't leak to another function
+        with pytest.raises(AssertionError) as exc_info:
+            run_strict()
+
+        # check that one URL is in uncalled assertion
+        assert "https://notcalled1.com/" in str(exc_info.value)
+
+        run_classic()
+        run_not_strict()
+
+    @pytest.mark.parametrize("assert_fired", (True, False, None))
+    def test_nested_decorators(self, assert_fired):
+        """Validate if assert_all_requests_are_fired is applied from the correct function.
+
+        assert_all_requests_are_fired must be applied from the function
+        where call to 'requests' is done.
+        Test matrix of True/False/None values applied to validate different scenarios.
+        """
+
+        @responses.activate(assert_all_requests_are_fired=assert_fired)
+        def wrapped():
+            responses.add(responses.GET, "https://notcalled1.com/", "success")
+            responses.add(responses.GET, "http://example.com/1", body="Hello 1")
+            assert b"Hello 1" == requests.get("http://example.com/1").content
+
+        @responses.activate(assert_all_requests_are_fired=not assert_fired)
+        def call_another_wrapped_function():
+            responses.add(responses.GET, "https://notcalled2.com/", "success")
+            wrapped()
+
+        if assert_fired:
+            with pytest.raises(AssertionError) as exc_info:
+                call_another_wrapped_function()
+
+            assert "https://notcalled1.com/" in str(exc_info.value)
+            assert "https://notcalled2.com/" in str(exc_info.value)
+        else:
+            call_another_wrapped_function()
+
+
 class TestMultipleWrappers:
     """Test to validate that multiple decorators could be applied.
 
@@ -2166,6 +2234,60 @@ class TestShortcuts:
         assert_reset()
 
 
+class TestUnitTestPatchSetup:
+    """Validates that ``RequestsMock`` could be used as ``mock.patch``.
+
+    This class is present as example in README.rst
+
+    """
+
+    def setup(self):
+        self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True)
+        self.r_mock.start()
+        self.r_mock.get("https://example.com", status=505)
+        self.r_mock.put("https://example.com", status=506)
+
+    def teardown(self):
+        self.r_mock.stop()
+        self.r_mock.reset()
+
+        assert_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
+
+
+class TestUnitTestPatchSetupRaises:
+    """Validate that teardown raises if not all requests were executed.
+
+    Similar to ``TestUnitTestPatchSetup``.
+
+    """
+
+    def setup(self):
+        self.r_mock = responses.RequestsMock()
+        self.r_mock.start()
+        self.r_mock.get("https://example.com", status=505)
+        self.r_mock.put("https://example.com", status=506)
+
+    def teardown(self):
+        with pytest.raises(AssertionError) as exc:
+            self.r_mock.stop()
+        self.r_mock.reset()
+
+        assert "[('PUT', 'https://example.com/')]" in str(exc.value)
+
+        assert_reset()
+
+    def test_function(self):
+        resp = requests.get("https://example.com")
+        assert resp.status_code == 505
+
+
 def test_reset_in_the_middle():
     @responses.activate
     def run():
@@ -2223,3 +2345,114 @@ def test_redirect():
 
     run()
     assert_reset()
+
+
+class TestMaxRetry:
+    def test_max_retries(self):
+        """This example is present in README.rst"""
+
+        @responses.activate(registry=registries.OrderedRegistry)
+        def run():
+            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
+
+        run()
+        assert_reset()
+
+    @pytest.mark.parametrize("raise_on_status", (True, False))
+    def test_max_retries_exceed(self, raise_on_status):
+        @responses.activate(registry=registries.OrderedRegistry)
+        def run():
+            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)
+
+            session = requests.Session()
+
+            adapter = requests.adapters.HTTPAdapter(
+                max_retries=Retry(
+                    total=2,
+                    backoff_factor=0.1,
+                    status_forcelist=[500],
+                    method_whitelist=["GET", "POST", "PATCH"],
+                    raise_on_status=raise_on_status,
+                )
+            )
+            session.mount("https://", adapter)
+
+            if raise_on_status:
+                with pytest.raises(MaxRetryError):
+                    session.get(url)
+            else:
+                resp = session.get(url)
+                assert resp.status_code == 500
+
+            assert rsp1.call_count == 1
+            assert rsp2.call_count == 1
+            assert rsp3.call_count == 1
+
+        run()
+        assert_reset()
+
+    def test_adapter_retry_untouched(self):
+        """Validate that every new request uses brand-new Retry object"""
+
+        @responses.activate(registry=registries.OrderedRegistry)
+        def run():
+            url = "https://example.com"
+            error_rsp = responses.get(url, body="Error", status=500)
+            responses.add(error_rsp)
+            responses.add(error_rsp)
+            ok_rsp = responses.get(url, body="OK", status=200)
+
+            responses.add(error_rsp)
+            responses.add(error_rsp)
+            responses.add(error_rsp)
+            responses.add(ok_rsp)
+
+            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
+
+            resp = session.get(url)
+            assert resp.status_code == 200
+
+            assert len(responses.calls) == 8
+
+        run()
+        assert_reset()