diff --git a/NEWS b/NEWS index 713cdf1..25011f8 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,10 @@ Improvements * Correctly display non-ASCII unicode output on terminals that claim to have a unicode encoding. (Martin [gz], #804122) +* ``ExpectedException`` now matches any exception of the given type by + default, and also allows specifying a ``Matcher`` rather than a mere regular + expression. (Jonathan Lange, #791889) + * ``FixtureSuite`` added, allows test suites to run with a given fixture. (Jonathan Lange) diff --git a/doc/for-test-authors.rst b/doc/for-test-authors.rst index 7f71e82..df19452 100644 --- a/doc/for-test-authors.rst +++ b/doc/for-test-authors.rst @@ -160,9 +160,10 @@ particular errors. ``ExpectedException`` does just that. For example:: silly.square('orange') The first argument to ``ExpectedException`` is the type of exception you -expect to see raised. The second argument is an optional regular expression, -if provided, the ``str()`` of the raised exception must match the regular -expression. +expect to see raised. The second argument is optional, and can be either a +regular expression or a matcher. If it is a regular expression, the ``str()`` +of the raised exception must match the regular expression. If it is a matcher, +then the raised exception object must match it. assertIn, assertNotIn diff --git a/testtools/matchers.py b/testtools/matchers.py index 8264a94..1c40d41 100644 --- a/testtools/matchers.py +++ b/testtools/matchers.py @@ -421,7 +421,7 @@ class MatchesException(Matcher): Matcher.__init__(self) self.expected = exception if istext(value_re): - value_re = AfterPreproccessing(str, MatchesRegex(value_re)) + value_re = AfterPreproccessing(str, MatchesRegex(value_re), False) self.value_re = value_re self._is_instance = type(self.expected) not in classtypes() @@ -824,9 +824,19 @@ class AfterPreprocessing(object): return AfterPreprocessing(_read, Equals(content)) """ - def __init__(self, preprocessor, matcher): + def __init__(self, preprocessor, matcher, annotate=True): + """Create an AfterPreprocessing matcher. + + :param preprocessor: A function called with the matchee before + matching. + :param matcher: What to match the preprocessed matchee against. + :param annotate: Whether or not to annotate the matcher with + something explaining how we transformed the matchee. Defaults + to True. + """ self.preprocessor = preprocessor self.matcher = matcher + self.annotate = annotate def _str_preprocessor(self): if isinstance(self.preprocessor, types.FunctionType): @@ -839,9 +849,13 @@ class AfterPreprocessing(object): def match(self, value): after = self.preprocessor(value) - return Annotate( - "after %s on %r" % (self._str_preprocessor(), value), - self.matcher).match(after) + if self.annotate: + matcher = Annotate( + "after %s on %r" % (self._str_preprocessor(), value), + self.matcher) + else: + matcher = self.matcher + return matcher.match(after) # This is the old, deprecated. spelling of the name, kept for backwards # compatibility. diff --git a/testtools/testcase.py b/testtools/testcase.py index 8f0f8ca..c638fa8 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -16,7 +16,6 @@ __all__ = [ import copy import itertools -import re import sys import types import unittest @@ -29,6 +28,7 @@ from testtools.compat import advance_iterator from testtools.matchers import ( Annotate, Equals, + MatchesException, Is, Not, ) @@ -754,7 +754,7 @@ class ExpectedException: exception is raised, an AssertionError will be raised. """ - def __init__(self, exc_type, value_re): + def __init__(self, exc_type, value_re=None): """Construct an `ExpectedException`. :param exc_type: The type of exception to expect. @@ -772,9 +772,11 @@ class ExpectedException: raise AssertionError('%s not raised.' % self.exc_type.__name__) if exc_type != self.exc_type: return False - if not re.match(self.value_re, str(exc_value)): - raise AssertionError('"%s" does not match "%s".' % - (str(exc_value), self.value_re)) + if self.value_re: + matcher = MatchesException(self.exc_type, self.value_re) + mismatch = matcher.match((exc_type, exc_value, traceback)) + if mismatch: + raise AssertionError(mismatch.describe()) return True diff --git a/testtools/tests/test_matchers.py b/testtools/tests/test_matchers.py index ecbe3fa..d17a053 100644 --- a/testtools/tests/test_matchers.py +++ b/testtools/tests/test_matchers.py @@ -258,10 +258,8 @@ class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface): MatchesException(Exception, 'fo.')) ] describe_examples = [ - # XXX: This is kind of a crappy message. Need to change - # AfterPreproccessing. - ("'bar' does not match 'fo.': after %r on %r" - % (str, error_bar[1]), error_bar, MatchesException(ValueError, "fo.")), + ("'bar' does not match 'fo.'", + error_bar, MatchesException(ValueError, "fo.")), ] @@ -736,9 +734,10 @@ class TestAfterPreprocessing(TestCase, TestMatchersInterface): ] describe_examples = [ - ("1 != 0: after on 2", - 2, + ("1 != 0: after on 2", 2, AfterPreprocessing(parity, Equals(1))), + ("1 != 0", 2, + AfterPreprocessing(parity, Equals(1), annotate=False)), ] diff --git a/testtools/tests/test_with_with.py b/testtools/tests/test_with_with.py index 3220882..23ce203 100644 --- a/testtools/tests/test_with_with.py +++ b/testtools/tests/test_with_with.py @@ -8,6 +8,11 @@ from testtools import ( ExpectedException, TestCase, ) +from testtools.matchers import ( + AfterPreprocessing, + Equals, + ) + class TestExpectedException(TestCase): """Test the ExpectedException context manager.""" @@ -16,13 +21,30 @@ class TestExpectedException(TestCase): with ExpectedException(ValueError, 'tes.'): raise ValueError('test') + def test_pass_on_raise_matcher(self): + with ExpectedException( + ValueError, AfterPreprocessing(str, Equals('test'))): + raise ValueError('test') + def test_raise_on_text_mismatch(self): try: with ExpectedException(ValueError, 'tes.'): raise ValueError('mismatch') except AssertionError: e = sys.exc_info()[1] - self.assertEqual('"mismatch" does not match "tes.".', str(e)) + self.assertEqual("'mismatch' does not match 'tes.'", str(e)) + else: + self.fail('AssertionError not raised.') + + def test_raise_on_general_mismatch(self): + matcher = AfterPreprocessing(str, Equals('test')) + value_error = ValueError('mismatch') + try: + with ExpectedException(ValueError, matcher): + raise value_error + except AssertionError: + e = sys.exc_info()[1] + self.assertEqual(matcher.match(value_error).describe(), str(e)) else: self.fail('AssertionError not raised.') @@ -45,3 +67,7 @@ class TestExpectedException(TestCase): self.assertEqual('TypeError not raised.', str(e)) else: self.fail('AssertionError not raised.') + + def test_pass_on_raise_any_message(self): + with ExpectedException(ValueError): + raise ValueError('whatever')