Skip to content

Commit 68e92ff

Browse files
authored
Merge pull request #443 from jelmer/assertraises-cm
Support TestCase.assertRaises being used as a context manager
2 parents 4a9d9da + 4669c5a commit 68e92ff

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

LICENSE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The testtools authors are:
2121
* Tristan Seligmann
2222
* Julian Edwards
2323
* Jonathan Jacobs
24+
* Jelmer Vernooij
2425

2526
and are collectively referred to as "testtools developers".
2627

@@ -42,7 +43,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4243
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4344
SOFTWARE.
4445

45-
Some code in testtools/run.py taken from Python's unittest module:
46+
Some code in testtools/run.py and testtools/testcase.py taken from Python's unittest module:
4647
Copyright (c) 1999-2003 Steve Purcell
4748
Copyright (c) 2003-2010 Python Software Foundation
4849

testtools/testcase.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,14 +436,32 @@ def assertIsInstance(self, obj, klass, msg=None):
436436
matcher = IsInstance(klass)
437437
self.assertThat(obj, matcher, msg)
438438

439-
def assertRaises(self, excClass, callableObj, *args, **kwargs):
439+
def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
440440
"""Fail unless an exception of class excClass is thrown
441441
by callableObj when invoked with arguments args and keyword
442442
arguments kwargs. If a different type of exception is
443443
thrown, it will not be caught, and the test case will be
444444
deemed to have suffered an error, exactly as for an
445445
unexpected exception.
446+
447+
If called with the callable omitted, will return a
448+
context object used like this::
449+
450+
with self.assertRaises(SomeException):
451+
do_something()
452+
453+
The context manager keeps a reference to the exception as
454+
the 'exception' attribute. This allows you to inspect the
455+
exception after the assertion::
456+
457+
with self.assertRaises(SomeException) as cm:
458+
do_something()
459+
the_exception = cm.exception
460+
self.assertEqual(the_exception.error_code, 3)
446461
"""
462+
# If callableObj is None, we're being used as a context manager
463+
if callableObj is None:
464+
return _AssertRaisesContext(excClass, self, msg=kwargs.get("msg"))
447465

448466
class ReRaiseOtherTypes:
449467
def match(self, matchee):
@@ -1011,6 +1029,51 @@ def _id(obj):
10111029
return _id
10121030

10131031

1032+
class _AssertRaisesContext:
1033+
"""A context manager to handle expected exceptions for assertRaises.
1034+
1035+
This provides compatibility with unittest's assertRaises context manager.
1036+
"""
1037+
1038+
def __init__(self, expected, test_case, msg=None):
1039+
"""Construct an `_AssertRaisesContext`.
1040+
1041+
:param expected: The type of exception to expect.
1042+
:param test_case: The TestCase instance using this context.
1043+
:param msg: An optional message explaining the failure.
1044+
"""
1045+
self.expected = expected
1046+
self.test_case = test_case
1047+
self.msg = msg
1048+
self.exception = None
1049+
1050+
def __enter__(self):
1051+
return self
1052+
1053+
def __exit__(self, exc_type, exc_value, traceback):
1054+
if exc_type is None:
1055+
try:
1056+
if isinstance(self.expected, tuple):
1057+
exc_name = "({})".format(
1058+
", ".join(e.__name__ for e in self.expected)
1059+
)
1060+
else:
1061+
exc_name = self.expected.__name__
1062+
except AttributeError:
1063+
exc_name = str(self.expected)
1064+
if self.msg:
1065+
error_msg = "{} not raised : {}".format(exc_name, self.msg)
1066+
else:
1067+
error_msg = "{} not raised".format(exc_name)
1068+
raise self.test_case.failureException(error_msg)
1069+
if not issubclass(exc_type, self.expected):
1070+
# let unexpected exceptions pass through
1071+
return False
1072+
# store exception for later retrieval
1073+
self.exception = exc_value
1074+
return True
1075+
1076+
10141077
class ExpectedException:
10151078
"""A context manager to handle expected exceptions.
10161079

testtools/tests/test_testcase.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,45 @@ def test_assertRaisesRegex_wrong_message(self):
457457
"Observed",
458458
)
459459

460+
def test_assertRaises_as_context_manager(self):
461+
# assertRaises can be used as a context manager
462+
with self.assertRaises(RuntimeError):
463+
raise RuntimeError("Error message")
464+
465+
def test_assertRaises_as_context_manager_no_exception(self):
466+
# assertRaises used as context manager fails when no exception is raised
467+
with self.assertRaises(self.failureException) as cm:
468+
with self.assertRaises(RuntimeError):
469+
pass
470+
self.assertIn("RuntimeError not raised", str(cm.exception))
471+
472+
def test_assertRaises_as_context_manager_wrong_exception(self):
473+
# assertRaises used as context manager re-raises unexpected exceptions
474+
with self.assertRaises(ZeroDivisionError):
475+
with self.assertRaises(RuntimeError):
476+
raise ZeroDivisionError("Wrong exception")
477+
478+
def test_assertRaises_as_context_manager_with_msg(self):
479+
# assertRaises context manager can accept a msg parameter
480+
with self.assertRaises(self.failureException) as cm:
481+
with self.assertRaises(RuntimeError, msg="Custom message"):
482+
pass
483+
self.assertIn("Custom message", str(cm.exception))
484+
485+
def test_assertRaises_as_context_manager_stores_exception(self):
486+
# assertRaises context manager stores the caught exception
487+
with self.assertRaises(RuntimeError) as cm:
488+
raise RuntimeError("Test error")
489+
self.assertIsInstance(cm.exception, RuntimeError)
490+
self.assertEqual("Test error", str(cm.exception))
491+
492+
def test_assertRaises_as_context_manager_with_multiple_exceptions(self):
493+
# assertRaises context manager works with multiple exception types
494+
exceptions = (RuntimeError, ValueError)
495+
with self.assertRaises(exceptions) as cm:
496+
raise ValueError("One of the expected exceptions")
497+
self.assertIsInstance(cm.exception, ValueError)
498+
460499
def assertFails(self, message, function, *args, **kwargs):
461500
"""Assert that function raises a failure with the given message."""
462501
failure = self.assertRaises(self.failureException, function, *args, **kwargs)

0 commit comments

Comments
 (0)