Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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
------

Expand Down
25 changes: 25 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------

Expand Down
10 changes: 4 additions & 6 deletions responses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()

Expand All @@ -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
"""

Expand Down
65 changes: 63 additions & 2 deletions responses/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization

The string [not-called.com](1) may be at an arbitrary position in the sanitized URL.
# 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)

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization

The string [not-called.com](1) may be at an arbitrary position in the sanitized URL.

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)

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization

The string [not-called.com](1) may be at an arbitrary position in the sanitized URL.
# 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"
Expand Down