Merge branch that provides syntax for specifying test runner.
This commit is contained in:
43
MANUAL
43
MANUAL
@@ -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
6
NEWS
@@ -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
|
||||
~~~~~
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user