Repository: getsentry/responses

Issue:
Allow applying @responses.activate to a class
Right now I have to apply the decorator to every method I want to mock responses in; I'd prefer to apply the decorator only to the `TestCase` declaration as my `TestCase` does http in every test method. This would keep requests consistent with the mock.patch behaviour.

E.g.:

```
@responses.activate
class CommandHandlerTestCase(unittest.TestCase):

    def test_function_1(self):
        responses.add(...)

    def test_function_2(self):
        responses.add(...)

    def test_function_3(self):
        responses.add(...)
```


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

```diff
@@            Coverage Diff            @@
##            master      #531   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         6           
  Lines         2320      2348   +28     
=========================================
+ Hits          2320      2348   +28     
```


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

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/531?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/531?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [7842ec3...db64b46](https://app.codecov.io/gh/getsentry/responses/compare/531?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/CHANGES b/CHANGES
index e916719..2a0cda2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,6 +5,8 @@
 * Removed internal `_cookies_from_headers` function
 * Now `add`, `upsert`, `replace` methods return registered response.
   `remove` method returns list of removed responses.
+* 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/README.rst b/README.rst
index 94b960f..81b294b 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
 --------------------------------
 
diff --git a/responses/__init__.py b/responses/__init__.py
index 460d24e..4af8013 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -132,7 +132,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 +151,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 +163,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
@@ -790,12 +807,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
 
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


Test patch:
diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py
index 47479bb..544a7aa 100644
--- a/responses/tests/test_responses.py
+++ b/responses/tests/test_responses.py
@@ -2059,6 +2059,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 +2231,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():