Merge branch that provides syntax for specifying test runner.

This commit is contained in:
Jonathan Lange
2010-10-17 17:34:55 +01:00
6 changed files with 187 additions and 22 deletions

43
MANUAL
View File

@@ -11,11 +11,12 @@ to the API docs (i.e. docstrings) for full details on a particular feature.
Extensions to TestCase
----------------------
Controlling test execution
~~~~~~~~~~~~~~~~~~~~~~~~~~
Custom exception handling
~~~~~~~~~~~~~~~~~~~~~~~~~
Testtools supports two ways to control how tests are executed. The simplest
is to add a new exception to self.exception_handlers::
testtools provides a way to control how test exceptions are handled. To do
this, add a new exception to self.exception_handlers on a TestCase. For
example::
>>> self.exception_handlers.insert(-1, (ExceptionClass, handler)).
@@ -23,12 +24,36 @@ Having done this, if any of setUp, tearDown, or the test method raise
ExceptionClass, handler will be called with the test case, test result and the
raised exception.
Secondly, by overriding __init__ to pass in runTest=RunTestFactory the whole
execution of the test can be altered. The default is testtools.runtest.RunTest
and calls case._run_setup, case._run_test_method and finally
case._run_teardown. Other methods to control what RunTest is used may be
added in future.
Controlling test execution
~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to control more than just how exceptions are raised, you can
provide a custom `RunTest` to a TestCase. The `RunTest` object can change
everything about how the test executes.
To work with `testtools.TestCase`, a `RunTest` must have a factory that takes
a test and an optional list of exception handlers. Instances returned by the
factory must have a `run()` method that takes an optional `TestResult` object.
The default is `testtools.runtest.RunTest` and calls 'setUp', the test method
and 'tearDown' in the normal, vanilla way that Python's standard unittest
does.
To specify a `RunTest` for all the tests in a `TestCase` class, do something
like this::
class SomeTests(TestCase):
run_tests_with = CustomRunTestFactory
To specify a `RunTest` for a specific test in a `TestCase` class, do::
class SomeTests(TestCase):
@run_test_with(CustomRunTestFactory, extra_arg=42, foo='whatever')
def test_something(self):
pass
In addition, either of these can be overridden by passing a factory in to the
`TestCase` constructor with the optional 'runTest' argument.
TestCase.addCleanup
~~~~~~~~~~~~~~~~~~~

6
NEWS
View File

@@ -7,6 +7,12 @@ NEXT
* Experimental support for running tests that return Deferreds.
(Jonathan Lange)
* Provide a per-test decoractor, run_test_with, to specify which RunTest
object to use for a given test. (Jonathan Lange, #657780)
* Fix the runTest parameter of TestCase to actually work, rather than raising
a TypeError. (Jonathan Lange, #657760)
0.9.7
~~~~~

View File

@@ -11,6 +11,7 @@ __all__ = [
'MultipleExceptions',
'MultiTestResult',
'PlaceHolder',
'run_test_with',
'TestCase',
'TestResult',
'TextTestResult',
@@ -33,6 +34,7 @@ from testtools.testcase import (
PlaceHolder,
TestCase,
clone_test_with_new_id,
run_test_with,
skip,
skipIf,
skipUnless,

View File

@@ -6,10 +6,11 @@ __metaclass__ = type
__all__ = [
'clone_test_with_new_id',
'MultipleExceptions',
'TestCase',
'run_test_with',
'skip',
'skipIf',
'skipUnless',
'TestCase',
]
import copy
@@ -61,6 +62,29 @@ except ImportError:
"""
def run_test_with(test_runner, **kwargs):
"""Decorate a test as using a specific `RunTest`.
e.g.
@run_test_with(CustomRunner, timeout=42)
def test_foo(self):
self.assertTrue(True)
: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.
"""
def make_test_runner(case, handlers=None):
return test_runner(case, handlers=handlers, **kwargs)
def decorator(f):
f._run_test_with = make_test_runner
return f
return decorator
class MultipleExceptions(Exception):
"""Represents many exceptions raised from some operation.
@@ -74,19 +98,25 @@ class TestCase(unittest.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
and an optional list of exception handlers.
"""
skipException = TestSkipped
run_tests_with = RunTest
def __init__(self, *args, **kwargs):
"""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
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.
"""
self.__RunTest = kwargs.pop('runTest', RunTest)
runTest = kwargs.pop('runTest', None)
unittest.TestCase.__init__(self, *args, **kwargs)
self._cleanups = []
self._unique_id_gen = itertools.count(1)
@@ -96,6 +126,11 @@ class TestCase(unittest.TestCase):
# __details is lazy-initialized so that a constructed-but-not-run
# TestCase is safe to use with clone_test_with_new_id.
self.__details = None
test_method = self._get_test_method()
if runTest is None:
runTest = getattr(
test_method, '_run_test_with', self.run_tests_with)
self.__RunTest = runTest
self.__exception_handlers = []
self.exception_handlers = [
(self.skipException, self._report_skip),
@@ -473,20 +508,22 @@ class TestCase(unittest.TestCase):
% self.__class__.__name__)
return ret
def _run_test_method(self, result):
"""Run the test method for this test.
:param result: A testtools.TestResult to report activity to.
:return: None.
"""
def _get_test_method(self):
absent_attr = object()
# Python 2.5+
method_name = getattr(self, '_testMethodName', absent_attr)
if method_name is absent_attr:
# Python 2.4
method_name = getattr(self, '_TestCase__testMethodName')
testMethod = getattr(self, method_name)
return testMethod()
return getattr(self, method_name)
def _run_test_method(self, result):
"""Run the test method for this test.
:param result: A testtools.TestResult to report activity to.
:return: None.
"""
return self._get_test_method()()
def setUp(self):
unittest.TestCase.setUp(self)

View File

@@ -4,10 +4,12 @@
from testtools import (
ExtendedToOriginalDecorator,
run_test_with,
RunTest,
TestCase,
TestResult,
)
from testtools.matchers import Is
from testtools.tests.helpers import ExtendedTestResult
@@ -176,6 +178,99 @@ class TestRunTest(TestCase):
], result._events)
class CustomRunTest(RunTest):
marker = object()
def run(self, result=None):
return self.marker
class TestTestCaseSupportForRunTest(TestCase):
def test_pass_custom_run_test(self):
class SomeCase(TestCase):
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo', runTest=CustomRunTest)
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(CustomRunTest.marker))
def test_default_is_runTest_class_variable(self):
class SomeCase(TestCase):
run_tests_with = CustomRunTest
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo')
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(CustomRunTest.marker))
def test_constructor_argument_overrides_class_variable(self):
# If a 'runTest' argument is passed to the test's constructor, that
# overrides the class variable.
marker = object()
class DifferentRunTest(RunTest):
def run(self, result=None):
return marker
class SomeCase(TestCase):
run_tests_with = CustomRunTest
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo', runTest=DifferentRunTest)
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(marker))
def test_decorator_for_run_test(self):
# Individual test methods can be marked as needing a special runner.
class SomeCase(TestCase):
@run_test_with(CustomRunTest)
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo')
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(CustomRunTest.marker))
def test_extended_decorator_for_run_test(self):
# Individual test methods can be marked as needing a special runner.
# Extra arguments can be passed to the decorator which will then be
# passed on to the RunTest object.
marker = object()
class FooRunTest(RunTest):
def __init__(self, case, handlers=None, bar=None):
super(FooRunTest, self).__init__(case, handlers)
self.bar = bar
def run(self, result=None):
return self.bar
class SomeCase(TestCase):
@run_test_with(FooRunTest, bar=marker)
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo')
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(marker))
def test_constructor_overrides_decorator(self):
# If a 'runTest' argument is passed to the test's constructor, that
# overrides the decorator.
marker = object()
class DifferentRunTest(RunTest):
def run(self, result=None):
return marker
class SomeCase(TestCase):
@run_test_with(CustomRunTest)
def test_foo(self):
pass
result = TestResult()
case = SomeCase('test_foo', runTest=DifferentRunTest)
from_run_test = case.run(result)
self.assertThat(from_run_test, Is(marker))
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)

View File

@@ -406,7 +406,7 @@ Traceback (most recent call last):
File "...testtools...runtest.py", line ..., in _run_user...
return fn(*args)
File "...testtools...testcase.py", line ..., in _run_test_method
return testMethod()
return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in error
1/0
ZeroDivisionError:... divi... by zero...
@@ -420,7 +420,7 @@ Traceback (most recent call last):
File "...testtools...runtest.py", line ..., in _run_user...
return fn(*args)
File "...testtools...testcase.py", line ..., in _run_test_method
return testMethod()
return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in failed
self.fail("yo!")
AssertionError: yo!