Skip to content

Commit 8cff1de

Browse files
RJPercivalclaude
andcommitted
Add decorator support for assert_on_exception
The @responses.activate decorator now accepts an assert_on_exception parameter, providing a convenient way to enable assertion checking even when exceptions occur: @responses.activate( assert_all_requests_are_fired=True, assert_on_exception=True ) def test_my_api(): ... This is consistent with the existing decorator support for assert_all_requests_are_fired and registry parameters. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 66ce1cb commit 8cff1de

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

responses/__init__.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def get_wrapped(
179179
*,
180180
registry: Optional[Any] = None,
181181
assert_all_requests_are_fired: Optional[bool] = None,
182+
assert_on_exception: Optional[bool] = None,
182183
) -> Callable[..., Any]:
183184
"""Wrap provided function inside ``responses`` context manager.
184185
@@ -195,6 +196,8 @@ def get_wrapped(
195196
Custom registry that should be applied. See ``responses.registries``
196197
assert_all_requests_are_fired : bool
197198
Raise an error if not all registered responses were executed.
199+
assert_on_exception : bool
200+
Raise assertion errors even when an exception occurs in the context manager.
198201
199202
Returns
200203
-------
@@ -208,14 +211,20 @@ def get_wrapped(
208211
new=assert_all_requests_are_fired,
209212
)
210213

214+
assert_on_exception_mock = std_mock.patch.object(
215+
target=responses,
216+
attribute="assert_on_exception",
217+
new=assert_on_exception,
218+
)
219+
211220
if inspect.iscoroutinefunction(func):
212221
# set asynchronous wrapper if requestor function is asynchronous
213222
@wraps(func)
214223
async def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
215224
if registry is not None:
216225
responses._set_registry(registry)
217226

218-
with assert_mock, responses:
227+
with assert_mock, assert_on_exception_mock, responses:
219228
return await func(*args, **kwargs)
220229

221230
else:
@@ -225,8 +234,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
225234
if registry is not None:
226235
responses._set_registry(registry)
227236

228-
with assert_mock, responses:
229-
# set 'assert_all_requests_are_fired' temporarily for a single run.
237+
with assert_mock, assert_on_exception_mock, responses:
238+
# set 'assert_all_requests_are_fired' and 'assert_on_exception' temporarily for a single run.
230239
# Mock automatically unsets to avoid leakage to another decorated
231240
# function since we still apply the value on 'responses.mock' object
232241
return func(*args, **kwargs)
@@ -1009,9 +1018,9 @@ def activate(
10091018
*,
10101019
registry: Type[Any] = ...,
10111020
assert_all_requests_are_fired: bool = ...,
1021+
assert_on_exception: bool = ...,
10121022
) -> Callable[["_F"], "_F"]:
1013-
"""Overload for scenario when
1014-
'responses.activate(registry=, assert_all_requests_are_fired=True)' is used.
1023+
"""Overload for scenario when 'responses.activate(...)' is used.
10151024
See https://github.com/getsentry/responses/pull/469 for more details
10161025
"""
10171026

@@ -1021,6 +1030,7 @@ def activate(
10211030
*,
10221031
registry: Optional[Type[Any]] = None,
10231032
assert_all_requests_are_fired: bool = False,
1033+
assert_on_exception: bool = False,
10241034
) -> Union[Callable[["_F"], "_F"], "_F"]:
10251035
if func is not None:
10261036
return get_wrapped(func, self)
@@ -1031,6 +1041,7 @@ def deco_activate(function: "_F") -> Callable[..., Any]:
10311041
self,
10321042
registry=registry,
10331043
assert_all_requests_are_fired=assert_all_requests_are_fired,
1044+
assert_on_exception=assert_on_exception,
10341045
)
10351046

10361047
return deco_activate

responses/tests/test_responses.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,48 @@ def run():
12681268
assert_reset()
12691269

12701270

1271+
def test_assert_on_exception_with_decorator():
1272+
"""Test that assert_on_exception works with the @responses.activate decorator."""
1273+
1274+
# Default behavior with decorator: assertion should NOT be raised when an exception occurs
1275+
with pytest.raises(ValueError) as value_exc_info:
1276+
1277+
@responses.activate(assert_all_requests_are_fired=True)
1278+
def test_default():
1279+
responses.add(responses.GET, "http://example.com", body=b"test")
1280+
responses.add(responses.GET, "http://not-called.com", body=b"test")
1281+
requests.get("http://example.com")
1282+
raise ValueError("Main error")
1283+
1284+
test_default()
1285+
1286+
# Should only see the ValueError, not the AssertionError about unfired requests
1287+
assert "Main error" in str(value_exc_info.value)
1288+
assert "not-called.com" not in str(value_exc_info.value)
1289+
1290+
# With assert_on_exception=True in decorator: assertion WILL be raised even with an exception
1291+
with pytest.raises(AssertionError) as assert_exc_info:
1292+
1293+
@responses.activate(
1294+
assert_all_requests_are_fired=True, assert_on_exception=True
1295+
)
1296+
def test_with_assert_on_exception():
1297+
responses.add(responses.GET, "http://example.com", body=b"test")
1298+
responses.add(responses.GET, "http://not-called.com", body=b"test")
1299+
requests.get("http://example.com")
1300+
raise ValueError("Main error")
1301+
1302+
test_with_assert_on_exception()
1303+
1304+
# The AssertionError should mention the unfired request
1305+
assert "not-called.com" in str(assert_exc_info.value)
1306+
# Python automatically chains exceptions, so we should see both in the traceback
1307+
assert isinstance(assert_exc_info.value.__context__, ValueError)
1308+
assert "Main error" in str(assert_exc_info.value.__context__)
1309+
1310+
assert_reset()
1311+
1312+
12711313
def test_allow_redirects_samehost():
12721314
redirecting_url = "http://example.com"
12731315
final_url_path = "/1"

0 commit comments

Comments
 (0)