ExpectedException now more useful.

This commit is contained in:
Jonathan Lange
2011-07-28 10:58:13 +01:00
6 changed files with 66 additions and 20 deletions

4
NEWS
View File

@@ -39,6 +39,10 @@ Improvements
* Correctly display non-ASCII unicode output on terminals that claim to have a * Correctly display non-ASCII unicode output on terminals that claim to have a
unicode encoding. (Martin [gz], #804122) 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. * ``FixtureSuite`` added, allows test suites to run with a given fixture.
(Jonathan Lange) (Jonathan Lange)

View File

@@ -160,9 +160,10 @@ particular errors. ``ExpectedException`` does just that. For example::
silly.square('orange') silly.square('orange')
The first argument to ``ExpectedException`` is the type of exception you The first argument to ``ExpectedException`` is the type of exception you
expect to see raised. The second argument is an optional regular expression, expect to see raised. The second argument is optional, and can be either a
if provided, the ``str()`` of the raised exception must match the regular regular expression or a matcher. If it is a regular expression, the ``str()``
expression. of the raised exception must match the regular expression. If it is a matcher,
then the raised exception object must match it.
assertIn, assertNotIn assertIn, assertNotIn

View File

@@ -421,7 +421,7 @@ class MatchesException(Matcher):
Matcher.__init__(self) Matcher.__init__(self)
self.expected = exception self.expected = exception
if istext(value_re): 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.value_re = value_re
self._is_instance = type(self.expected) not in classtypes() self._is_instance = type(self.expected) not in classtypes()
@@ -824,9 +824,19 @@ class AfterPreprocessing(object):
return AfterPreprocessing(_read, Equals(content)) 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.preprocessor = preprocessor
self.matcher = matcher self.matcher = matcher
self.annotate = annotate
def _str_preprocessor(self): def _str_preprocessor(self):
if isinstance(self.preprocessor, types.FunctionType): if isinstance(self.preprocessor, types.FunctionType):
@@ -839,9 +849,13 @@ class AfterPreprocessing(object):
def match(self, value): def match(self, value):
after = self.preprocessor(value) after = self.preprocessor(value)
return Annotate( if self.annotate:
"after %s on %r" % (self._str_preprocessor(), value), matcher = Annotate(
self.matcher).match(after) "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 # This is the old, deprecated. spelling of the name, kept for backwards
# compatibility. # compatibility.

View File

@@ -16,7 +16,6 @@ __all__ = [
import copy import copy
import itertools import itertools
import re
import sys import sys
import types import types
import unittest import unittest
@@ -29,6 +28,7 @@ from testtools.compat import advance_iterator
from testtools.matchers import ( from testtools.matchers import (
Annotate, Annotate,
Equals, Equals,
MatchesException,
Is, Is,
Not, Not,
) )
@@ -754,7 +754,7 @@ class ExpectedException:
exception is raised, an AssertionError will be raised. 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`. """Construct an `ExpectedException`.
:param exc_type: The type of exception to expect. :param exc_type: The type of exception to expect.
@@ -772,9 +772,11 @@ class ExpectedException:
raise AssertionError('%s not raised.' % self.exc_type.__name__) raise AssertionError('%s not raised.' % self.exc_type.__name__)
if exc_type != self.exc_type: if exc_type != self.exc_type:
return False return False
if not re.match(self.value_re, str(exc_value)): if self.value_re:
raise AssertionError('"%s" does not match "%s".' % matcher = MatchesException(self.exc_type, self.value_re)
(str(exc_value), self.value_re)) mismatch = matcher.match((exc_type, exc_value, traceback))
if mismatch:
raise AssertionError(mismatch.describe())
return True return True

View File

@@ -258,10 +258,8 @@ class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface):
MatchesException(Exception, 'fo.')) MatchesException(Exception, 'fo.'))
] ]
describe_examples = [ describe_examples = [
# XXX: This is kind of a crappy message. Need to change ("'bar' does not match 'fo.'",
# AfterPreproccessing. error_bar, MatchesException(ValueError, "fo.")),
("'bar' does not match 'fo.': after %r on %r"
% (str, error_bar[1]), error_bar, MatchesException(ValueError, "fo.")),
] ]
@@ -736,9 +734,10 @@ class TestAfterPreprocessing(TestCase, TestMatchersInterface):
] ]
describe_examples = [ describe_examples = [
("1 != 0: after <function parity> on 2", ("1 != 0: after <function parity> on 2", 2,
2,
AfterPreprocessing(parity, Equals(1))), AfterPreprocessing(parity, Equals(1))),
("1 != 0", 2,
AfterPreprocessing(parity, Equals(1), annotate=False)),
] ]

View File

@@ -8,6 +8,11 @@ from testtools import (
ExpectedException, ExpectedException,
TestCase, TestCase,
) )
from testtools.matchers import (
AfterPreprocessing,
Equals,
)
class TestExpectedException(TestCase): class TestExpectedException(TestCase):
"""Test the ExpectedException context manager.""" """Test the ExpectedException context manager."""
@@ -16,13 +21,30 @@ class TestExpectedException(TestCase):
with ExpectedException(ValueError, 'tes.'): with ExpectedException(ValueError, 'tes.'):
raise ValueError('test') 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): def test_raise_on_text_mismatch(self):
try: try:
with ExpectedException(ValueError, 'tes.'): with ExpectedException(ValueError, 'tes.'):
raise ValueError('mismatch') raise ValueError('mismatch')
except AssertionError: except AssertionError:
e = sys.exc_info()[1] 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: else:
self.fail('AssertionError not raised.') self.fail('AssertionError not raised.')
@@ -45,3 +67,7 @@ class TestExpectedException(TestCase):
self.assertEqual('TypeError not raised.', str(e)) self.assertEqual('TypeError not raised.', str(e))
else: else:
self.fail('AssertionError not raised.') self.fail('AssertionError not raised.')
def test_pass_on_raise_any_message(self):
with ExpectedException(ValueError):
raise ValueError('whatever')