diff --git a/CHANGES b/CHANGES index c1c617d7..526f8253 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,12 @@ +0.26.0 +------ + +* **Breaking change**: When using `assert_all_requests_are_fired=True`, assertions about + unfired requests are now raised even when an exception occurs in the context manager or + decorated function. Previously, these assertions were suppressed when exceptions occurred. + This new behavior provides valuable debugging context about which mocked requests were + or weren't called. + 0.25.8 ------ diff --git a/README.rst b/README.rst index a9c73e7d..a7919906 100644 --- a/README.rst +++ b/README.rst @@ -917,6 +917,31 @@ the ``assert_all_requests_are_fired`` value: content_type="application/json", ) +When ``assert_all_requests_are_fired=True`` and an exception occurs within the +context manager, assertions about unfired requests will still be raised. This +provides valuable context about which mocked requests were or weren't called +when debugging test failures. + +.. code-block:: python + + import responses + import requests + + + def test_with_exception(): + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: + rsps.add(responses.GET, "http://example.com/users", body="test") + rsps.add(responses.GET, "http://example.com/profile", body="test") + requests.get("http://example.com/users") + raise ValueError("Something went wrong") + + # Output: + # ValueError: Something went wrong + # + # During handling of the above exception, another exception occurred: + # + # AssertionError: Not all requests have been executed [('GET', 'http://example.com/profile')] + Assert Request Call Count ------------------------- diff --git a/responses/__init__.py b/responses/__init__.py index 89cc9a5c..ca2d17f8 100644 --- a/responses/__init__.py +++ b/responses/__init__.py @@ -226,8 +226,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc] responses._set_registry(registry) with assert_mock, responses: - # set 'assert_all_requests_are_fired' temporarily for a single run. - # Mock automatically unsets to avoid leakage to another decorated + # 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) @@ -991,9 +991,8 @@ def __enter__(self) -> "RequestsMock": return self def __exit__(self, type: Any, value: Any, traceback: Any) -> None: - success = type is None try: - self.stop(allow_assert=success) + self.stop(allow_assert=True) finally: self.reset() @@ -1008,8 +1007,7 @@ def activate( registry: Type[Any] = ..., assert_all_requests_are_fired: bool = ..., ) -> Callable[["_F"], "_F"]: - """Overload for scenario when - 'responses.activate(registry=, assert_all_requests_are_fired=True)' is used. + """Overload for scenario when 'responses.activate(...)' is used. See https://github.com/getsentry/responses/pull/469 for more details """ diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py index 66711a51..0f877f43 100644 --- a/responses/tests/test_responses.py +++ b/responses/tests/test_responses.py @@ -1163,11 +1163,13 @@ def run(): with responses.RequestsMock() as m: m.add(responses.GET, "http://example.com", body=b"test") - # check that assert_all_requests_are_fired doesn't swallow exceptions - with pytest.raises(ValueError): + # check that assert_all_requests_are_fired raises assertions even with exceptions + with pytest.raises(AssertionError) as exc_info: with responses.RequestsMock() as m: m.add(responses.GET, "http://example.com", body=b"test") raise ValueError() + # The ValueError should be chained as the context + assert isinstance(exc_info.value.__context__, ValueError) # check that assert_all_requests_are_fired=True doesn't remove urls with responses.RequestsMock(assert_all_requests_are_fired=True) as m: @@ -1217,6 +1219,65 @@ def test_some_second_function(): assert_reset() +def test_assert_all_requests_are_fired_during_exception(): + """Test that assertions are raised even when an exception occurs.""" + + def run(): + # Assertions WILL be raised even with an exception + # The AssertionError will be the primary exception, with the ValueError as context + with pytest.raises(AssertionError) as assert_exc_info: + with responses.RequestsMock(assert_all_requests_are_fired=True) as m: + m.add(responses.GET, "http://example.com", body=b"test") + m.add(responses.GET, "http://not-called.com", body=b"test") + requests.get("http://example.com") + raise ValueError("Main error") + + # The AssertionError should mention the unfired request + assert "not-called.com" in str(assert_exc_info.value) + # Python automatically chains exceptions, so we should see both in the traceback + assert isinstance(assert_exc_info.value.__context__, ValueError) + assert "Main error" in str(assert_exc_info.value.__context__) + + # Test that it also works normally when no other exception occurs + with pytest.raises(AssertionError) as assert_exc_info2: + with responses.RequestsMock(assert_all_requests_are_fired=True) as m: + m.add(responses.GET, "http://example.com", body=b"test") + m.add(responses.GET, "http://not-called.com", body=b"test") + requests.get("http://example.com") + + assert "not-called.com" in str(assert_exc_info2.value) + + run() + assert_reset() + + +def test_assert_all_requests_are_fired_during_exception_with_decorator(): + """Test that assertions are raised even when an exception occurs. + + This tests the behavior with the @responses.activate decorator. + """ + + # Assertions WILL be raised even with an exception when using the decorator + with pytest.raises(AssertionError) as assert_exc_info: + + @responses.activate(assert_all_requests_are_fired=True) + def test_with_exception(): + responses.add(responses.GET, "http://example.com", body=b"test") + responses.add(responses.GET, "http://not-called.com", body=b"test") + requests.get("http://example.com") + raise ValueError("Main error") + + test_with_exception() + + # The AssertionError should mention the unfired request + assert "not-called.com" in str(assert_exc_info.value) + # Python automatically chains exceptions, so we should see both in the traceback + assert isinstance(assert_exc_info.value.__context__, ValueError) + assert "Main error" in str(assert_exc_info.value.__context__) + + assert_reset() + + def test_allow_redirects_samehost(): redirecting_url = "http://example.com" final_url_path = "/1"