summaryrefslogtreecommitdiff
path: root/lib/testtools/testtools/testcase.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/testtools/testtools/testcase.py')
-rw-r--r--lib/testtools/testtools/testcase.py264
1 files changed, 187 insertions, 77 deletions
diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py
index 804684adb8..9370b29e57 100644
--- a/lib/testtools/testtools/testcase.py
+++ b/lib/testtools/testtools/testcase.py
@@ -1,10 +1,12 @@
-# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Test case related stuff."""
__metaclass__ = type
__all__ = [
'clone_test_with_new_id',
+ 'ExpectedException',
+ 'gather_details',
'run_test_with',
'skip',
'skipIf',
@@ -22,10 +24,20 @@ from testtools import (
content,
try_import,
)
-from testtools.compat import advance_iterator
+from testtools.compat import (
+ advance_iterator,
+ reraise,
+ )
from testtools.matchers import (
Annotate,
+ Contains,
Equals,
+ MatchesAll,
+ MatchesException,
+ Is,
+ IsInstance,
+ Not,
+ Raises,
)
from testtools.monkey import patch
from testtools.runtest import RunTest
@@ -35,6 +47,7 @@ wraps = try_import('functools.wraps')
class TestSkipped(Exception):
"""Raised within TestCase.run() when a test is skipped."""
+testSkipped = try_import('unittest2.case.SkipTest', TestSkipped)
TestSkipped = try_import('unittest.case.SkipTest', TestSkipped)
@@ -45,6 +58,8 @@ class _UnexpectedSuccess(Exception):
module.
"""
_UnexpectedSuccess = try_import(
+ 'unittest2.case._UnexpectedSuccess', _UnexpectedSuccess)
+_UnexpectedSuccess = try_import(
'unittest.case._UnexpectedSuccess', _UnexpectedSuccess)
class _ExpectedFailure(Exception):
@@ -54,30 +69,33 @@ class _ExpectedFailure(Exception):
module.
"""
_ExpectedFailure = try_import(
+ 'unittest2.case._ExpectedFailure', _ExpectedFailure)
+_ExpectedFailure = try_import(
'unittest.case._ExpectedFailure', _ExpectedFailure)
def run_test_with(test_runner, **kwargs):
- """Decorate a test as using a specific `RunTest`.
+ """Decorate a test as using a specific ``RunTest``.
+
+ e.g.::
- e.g.
@run_test_with(CustomRunner, timeout=42)
def test_foo(self):
self.assertTrue(True)
The returned decorator works by setting an attribute on the decorated
- function. `TestCase.__init__` looks for this attribute when deciding
- on a `RunTest` factory. If you wish to use multiple decorators on a test
- method, then you must either make this one the top-most decorator, or
- you must write your decorators so that they update the wrapping function
- with the attributes of the wrapped function. The latter is recommended
- style anyway. `functools.wraps`, `functools.wrapper` and
- `twisted.python.util.mergeFunctionMetadata` can help you do this.
-
- :param test_runner: A `RunTest` factory that takes a test case and an
- optional list of exception handlers. See `RunTest`.
- :param **kwargs: Keyword arguments to pass on as extra arguments to
- `test_runner`.
+ function. `TestCase.__init__` looks for this attribute when deciding on a
+ ``RunTest`` factory. If you wish to use multiple decorators on a test
+ method, then you must either make this one the top-most decorator, or you
+ must write your decorators so that they update the wrapping function with
+ the attributes of the wrapped function. The latter is recommended style
+ anyway. ``functools.wraps``, ``functools.wrapper`` and
+ ``twisted.python.util.mergeFunctionMetadata`` can help you do this.
+
+ :param test_runner: A ``RunTest`` factory that takes a test case and an
+ optional list of exception handlers. See ``RunTest``.
+ :param kwargs: Keyword arguments to pass on as extra arguments to
+ 'test_runner'.
:return: A decorator to be used for marking a test as needing a special
runner.
"""
@@ -91,14 +109,45 @@ def run_test_with(test_runner, **kwargs):
return decorator
+def _copy_content(content_object):
+ """Make a copy of the given content object.
+
+ The content within `content_object` is iterated and saved. This is useful
+ when the source of the content is volatile, a log file in a temporary
+ directory for example.
+
+ :param content_object: A `content.Content` instance.
+ :return: A `content.Content` instance with the same mime-type as
+ `content_object` and a non-volatile copy of its content.
+ """
+ content_bytes = list(content_object.iter_bytes())
+ content_callback = lambda: content_bytes
+ return content.Content(content_object.content_type, content_callback)
+
+
+def gather_details(source_dict, target_dict):
+ """Merge the details from `source_dict` into `target_dict`.
+
+ :param source_dict: A dictionary of details will be gathered.
+ :param target_dict: A dictionary into which details will be gathered.
+ """
+ for name, content_object in source_dict.items():
+ new_name = name
+ disambiguator = itertools.count(1)
+ while new_name in target_dict:
+ new_name = '%s-%d' % (name, advance_iterator(disambiguator))
+ name = new_name
+ target_dict[name] = _copy_content(content_object)
+
+
class TestCase(unittest.TestCase):
"""Extensions to the basic TestCase.
:ivar exception_handlers: Exceptions to catch from setUp, runTest and
tearDown. This list is able to be modified at any time and consists of
(exception_class, handler(case, result, exception_value)) pairs.
- :cvar run_tests_with: A factory to make the `RunTest` to run tests with.
- Defaults to `RunTest`. The factory is expected to take a test case
+ :cvar run_tests_with: A factory to make the ``RunTest`` to run tests with.
+ Defaults to ``RunTest``. The factory is expected to take a test case
and an optional list of exception handlers.
"""
@@ -110,13 +159,13 @@ class TestCase(unittest.TestCase):
"""Construct a TestCase.
:param testMethod: The name of the method to run.
- :param runTest: Optional class to use to execute the test. If not
- supplied `testtools.runtest.RunTest` is used. The instance to be
- used is created when run() is invoked, so will be fresh each time.
- Overrides `run_tests_with` if given.
+ :keyword runTest: Optional class to use to execute the test. If not
+ supplied ``RunTest`` is used. The instance to be used is created
+ when run() is invoked, so will be fresh each time. Overrides
+ ``TestCase.run_tests_with`` if given.
"""
runTest = kwargs.pop('runTest', None)
- unittest.TestCase.__init__(self, *args, **kwargs)
+ super(TestCase, self).__init__(*args, **kwargs)
self._cleanups = []
self._unique_id_gen = itertools.count(1)
# Generators to ensure unique traceback ids. Maps traceback label to
@@ -263,16 +312,31 @@ class TestCase(unittest.TestCase):
:param message: An optional message to include in the error.
"""
matcher = Equals(expected)
- if message:
- matcher = Annotate(message, matcher)
- self.assertThat(observed, matcher)
+ self.assertThat(observed, matcher, message)
failUnlessEqual = assertEquals = assertEqual
def assertIn(self, needle, haystack):
"""Assert that needle is in haystack."""
- self.assertTrue(
- needle in haystack, '%r not in %r' % (needle, haystack))
+ self.assertThat(haystack, Contains(needle))
+
+ def assertIsNone(self, observed, message=''):
+ """Assert that 'observed' is equal to None.
+
+ :param observed: The observed value.
+ :param message: An optional message describing the error.
+ """
+ matcher = Is(None)
+ self.assertThat(observed, matcher, message)
+
+ def assertIsNotNone(self, observed, message=''):
+ """Assert that 'observed' is not equal to None.
+
+ :param observed: The observed value.
+ :param message: An optional message describing the error.
+ """
+ matcher = Not(Is(None))
+ self.assertThat(observed, matcher, message)
def assertIs(self, expected, observed, message=''):
"""Assert that 'expected' is 'observed'.
@@ -281,30 +345,25 @@ class TestCase(unittest.TestCase):
:param observed: The observed value.
:param message: An optional message describing the error.
"""
- if message:
- message = ': ' + message
- self.assertTrue(
- expected is observed,
- '%r is not %r%s' % (expected, observed, message))
+ matcher = Is(expected)
+ self.assertThat(observed, matcher, message)
def assertIsNot(self, expected, observed, message=''):
"""Assert that 'expected' is not 'observed'."""
- if message:
- message = ': ' + message
- self.assertTrue(
- expected is not observed,
- '%r is %r%s' % (expected, observed, message))
+ matcher = Not(Is(expected))
+ self.assertThat(observed, matcher, message)
def assertNotIn(self, needle, haystack):
"""Assert that needle is not in haystack."""
- self.assertTrue(
- needle not in haystack, '%r in %r' % (needle, haystack))
+ matcher = Not(Contains(needle))
+ self.assertThat(haystack, matcher)
def assertIsInstance(self, obj, klass, msg=None):
- if msg is None:
- msg = '%r is not an instance of %s' % (
- obj, self._formatTypes(klass))
- self.assertTrue(isinstance(obj, klass), msg)
+ if isinstance(klass, tuple):
+ matcher = IsInstance(*klass)
+ else:
+ matcher = IsInstance(klass)
+ self.assertThat(obj, matcher, msg)
def assertRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown
@@ -314,22 +373,29 @@ class TestCase(unittest.TestCase):
deemed to have suffered an error, exactly as for an
unexpected exception.
"""
- try:
- ret = callableObj(*args, **kwargs)
- except excClass:
- return sys.exc_info()[1]
- else:
- excName = self._formatTypes(excClass)
- self.fail("%s not raised, %r returned instead." % (excName, ret))
+ class ReRaiseOtherTypes(object):
+ def match(self, matchee):
+ if not issubclass(matchee[0], excClass):
+ reraise(*matchee)
+ class CaptureMatchee(object):
+ def match(self, matchee):
+ self.matchee = matchee[1]
+ capture = CaptureMatchee()
+ matcher = Raises(MatchesAll(ReRaiseOtherTypes(),
+ MatchesException(excClass), capture))
+
+ self.assertThat(lambda: callableObj(*args, **kwargs), matcher)
+ return capture.matchee
failUnlessRaises = assertRaises
- def assertThat(self, matchee, matcher):
+ def assertThat(self, matchee, matcher, message='', verbose=False):
"""Assert that matchee is matched by matcher.
:param matchee: An object to match with matcher.
:param matcher: An object meeting the testtools.Matcher protocol.
:raises self.failureException: When matcher does not match thing.
"""
+ matcher = Annotate.if_message(message, matcher)
mismatch = matcher.match(matchee)
if not mismatch:
return
@@ -341,8 +407,13 @@ class TestCase(unittest.TestCase):
full_name = "%s-%d" % (name, suffix)
suffix += 1
self.addDetail(full_name, content)
- self.fail('Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
- % (matchee, matcher, mismatch.describe()))
+ if verbose:
+ message = (
+ 'Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
+ % (matchee, matcher, mismatch.describe()))
+ else:
+ message = mismatch.describe()
+ self.fail(message)
def defaultTestResult(self):
return TestResult()
@@ -363,6 +434,7 @@ class TestCase(unittest.TestCase):
be removed. This separation preserves the original intent of the test
while it is in the expectFailure mode.
"""
+ # TODO: implement with matchers.
self._add_reason(reason)
try:
predicate(*args, **kwargs)
@@ -507,31 +579,23 @@ class TestCase(unittest.TestCase):
:return: The fixture, after setting it up and scheduling a cleanup for
it.
"""
- fixture.setUp()
- self.addCleanup(fixture.cleanUp)
- self.addCleanup(self._gather_details, fixture.getDetails)
- return fixture
-
- def _gather_details(self, getDetails):
- """Merge the details from getDetails() into self.getDetails()."""
- details = getDetails()
- my_details = self.getDetails()
- for name, content_object in details.items():
- new_name = name
- disambiguator = itertools.count(1)
- while new_name in my_details:
- new_name = '%s-%d' % (name, advance_iterator(disambiguator))
- name = new_name
- content_bytes = list(content_object.iter_bytes())
- content_callback = lambda:content_bytes
- self.addDetail(name,
- content.Content(content_object.content_type, content_callback))
+ try:
+ fixture.setUp()
+ except:
+ gather_details(fixture.getDetails(), self.getDetails())
+ raise
+ else:
+ self.addCleanup(fixture.cleanUp)
+ self.addCleanup(
+ gather_details, fixture.getDetails(), self.getDetails())
+ return fixture
def setUp(self):
- unittest.TestCase.setUp(self)
+ super(TestCase, self).setUp()
self.__setup_called = True
def tearDown(self):
+ super(TestCase, self).tearDown()
unittest.TestCase.tearDown(self)
self.__teardown_called = True
@@ -539,8 +603,8 @@ class TestCase(unittest.TestCase):
class PlaceHolder(object):
"""A placeholder test.
- `PlaceHolder` implements much of the same interface as `TestCase` and is
- particularly suitable for being added to `TestResult`s.
+ `PlaceHolder` implements much of the same interface as TestCase and is
+ particularly suitable for being added to TestResults.
"""
def __init__(self, test_id, short_description=None):
@@ -630,7 +694,7 @@ if types.FunctionType not in copy._copy_dispatch:
def clone_test_with_new_id(test, new_id):
- """Copy a TestCase, and give the copied test a new id.
+ """Copy a `TestCase`, and give the copied test a new id.
This is only expected to be used on tests that have been constructed but
not executed.
@@ -675,3 +739,49 @@ def skipUnless(condition, reason):
def _id(obj):
return obj
return _id
+
+
+class ExpectedException:
+ """A context manager to handle expected exceptions.
+
+ In Python 2.5 or later::
+
+ def test_foo(self):
+ with ExpectedException(ValueError, 'fo.*'):
+ raise ValueError('foo')
+
+ will pass. If the raised exception has a type other than the specified
+ type, it will be re-raised. If it has a 'str()' that does not match the
+ given regular expression, an AssertionError will be raised. If no
+ exception is raised, an AssertionError will be raised.
+ """
+
+ def __init__(self, exc_type, value_re=None):
+ """Construct an `ExpectedException`.
+
+ :param exc_type: The type of exception to expect.
+ :param value_re: A regular expression to match against the
+ 'str()' of the raised exception.
+ """
+ self.exc_type = exc_type
+ self.value_re = value_re
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is None:
+ raise AssertionError('%s not raised.' % self.exc_type.__name__)
+ if exc_type != self.exc_type:
+ return False
+ 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
+
+
+# Signal that this is part of the testing framework, and that code from this
+# should not normally appear in tracebacks.
+__unittest = True