From 2692fd4757699f0dd9fa40c5c604c38617ecd8a2 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 16:05:09 +0100 Subject: [PATCH 1/8] Make the runTest argument actually work. --- testtools/testcase.py | 3 ++- testtools/tests/test_runtest.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index 3471292..f387657 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -86,6 +86,7 @@ class TestCase(unittest.TestCase): supplied testtools.runtest.RunTest is used. The instance to be used is created when run() is invoked, so will be fresh each time. """ + runTest = kwargs.pop('runTest', RunTest) unittest.TestCase.__init__(self, *args, **kwargs) self._cleanups = [] self._unique_id_gen = itertools.count(1) @@ -95,7 +96,7 @@ 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 - self.__RunTest = kwargs.get('runTest', RunTest) + self.__RunTest = runTest self.__exception_handlers = [] self.exception_handlers = [ (self.skipException, self._report_skip), diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index a4c0a72..e01213a 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -8,6 +8,7 @@ from testtools import ( TestCase, TestResult, ) +from testtools.matchers import Is from testtools.tests.helpers import ExtendedTestResult @@ -176,6 +177,30 @@ class TestRunTest(TestCase): ], result._events) +class CustomRunTest(RunTest): + + def __init__(self, marker, *args, **kwargs): + super(CustomRunTest, self).__init__(*args, **kwargs) + self._marker = marker + + 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() + marker = object() + case = SomeCase( + 'test_foo', runTest=lambda *args: CustomRunTest(marker, *args)) + from_run_test = case.run(result) + self.assertThat(from_run_test, Is(marker)) + + def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) From 859a79aa84757716c943580779b05ba4e45a32fd Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 16:19:51 +0100 Subject: [PATCH 2/8] Add a class variable, run_tests_with, to allow controlling which runner to run tests with. --- testtools/testcase.py | 6 +++++- testtools/tests/test_runtest.py | 22 ++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index f387657..2ccde98 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -74,10 +74,14 @@ 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 `RunTest` class to run tests with. Defaults to + `RunTest`. """ skipException = TestSkipped + run_tests_with = RunTest + def __init__(self, *args, **kwargs): """Construct a TestCase. @@ -86,7 +90,7 @@ class TestCase(unittest.TestCase): supplied testtools.runtest.RunTest is used. The instance to be used is created when run() is invoked, so will be fresh each time. """ - runTest = kwargs.pop('runTest', RunTest) + runTest = kwargs.pop('runTest', self.run_tests_with) unittest.TestCase.__init__(self, *args, **kwargs) self._cleanups = [] self._unique_id_gen = itertools.count(1) diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index e01213a..f94af1d 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -179,12 +179,10 @@ class TestRunTest(TestCase): class CustomRunTest(RunTest): - def __init__(self, marker, *args, **kwargs): - super(CustomRunTest, self).__init__(*args, **kwargs) - self._marker = marker + marker = object() def run(self, result=None): - return self._marker + return self.marker class TestTestCaseSupportForRunTest(TestCase): @@ -194,11 +192,19 @@ class TestTestCaseSupportForRunTest(TestCase): def test_foo(self): pass result = TestResult() - marker = object() - case = SomeCase( - 'test_foo', runTest=lambda *args: CustomRunTest(marker, *args)) + case = SomeCase('test_foo', runTest=CustomRunTest) from_run_test = case.run(result) - self.assertThat(from_run_test, Is(marker)) + 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_suite(): From 84482c3ebc6a289e3052d9b58b3c8c062b64967c Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 16:46:01 +0100 Subject: [PATCH 3/8] Factor out the bit that gets the test method. --- testtools/testcase.py | 18 ++++++++++-------- testtools/tests/test_testresult.py | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index 2ccde98..109f6fa 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -476,20 +476,22 @@ class TestCase(unittest.TestCase): "super(%s, self).tearDown() from your tearDown()." % self.__class__.__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. - """ + 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) - 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. + """ + self._get_test_method()() def setUp(self): unittest.TestCase.setUp(self) diff --git a/testtools/tests/test_testresult.py b/testtools/tests/test_testresult.py index 9dc64df..cdc2115 100644 --- a/testtools/tests/test_testresult.py +++ b/testtools/tests/test_testresult.py @@ -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 - testMethod() + 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 - testMethod() + self._get_test_method()() File "...testtools...tests...test_testresult.py", line ..., in failed self.fail("yo!") AssertionError: yo! From aec8b4be4f12c50a124e8a55d18df349fc13bfc4 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 16:47:33 +0100 Subject: [PATCH 4/8] Add a decorator for specifying the RunTest object to use. --- testtools/__init__.py | 2 ++ testtools/testcase.py | 13 +++++++++++-- testtools/tests/test_runtest.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/testtools/__init__.py b/testtools/__init__.py index 60495c4..c13947b 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -11,6 +11,7 @@ __all__ = [ 'MultipleExceptions', 'MultiTestResult', 'PlaceHolder', + 'run_tests_with', 'TestCase', 'TestResult', 'TextTestResult', @@ -33,6 +34,7 @@ from testtools.testcase import ( PlaceHolder, TestCase, clone_test_with_new_id, + run_tests_with, skip, skipIf, skipUnless, diff --git a/testtools/testcase.py b/testtools/testcase.py index 109f6fa..4ba57e6 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -6,10 +6,11 @@ __metaclass__ = type __all__ = [ 'clone_test_with_new_id', 'MultipleExceptions', - 'TestCase', + 'run_tests_with', 'skip', 'skipIf', 'skipUnless', + 'TestCase', ] import copy @@ -61,6 +62,13 @@ except ImportError: """ +def run_tests_with(test_runner): + def decorator(f): + f._run_tests_with = test_runner + return f + return decorator + + class MultipleExceptions(Exception): """Represents many exceptions raised from some operation. @@ -100,7 +108,8 @@ 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 - self.__RunTest = runTest + test_method = self._get_test_method() + self.__RunTest = getattr(test_method, '_run_tests_with', runTest) self.__exception_handlers = [] self.exception_handlers = [ (self.skipException, self._report_skip), diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index f94af1d..ad2a3d9 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -4,6 +4,7 @@ from testtools import ( ExtendedToOriginalDecorator, + run_tests_with, RunTest, TestCase, TestResult, @@ -206,6 +207,16 @@ class TestTestCaseSupportForRunTest(TestCase): from_run_test = case.run(result) self.assertThat(from_run_test, Is(CustomRunTest.marker)) + def test_decorator_for_run_test(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_suite(): from unittest import TestLoader From 863cade82b52552076802812fb998aa76c06ffb3 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 16:53:21 +0100 Subject: [PATCH 5/8] Extend the decorator to take kwargs and pass them on. Documentation, and rename to grammatically correct run_test_with. --- testtools/__init__.py | 4 ++-- testtools/testcase.py | 24 ++++++++++++++++++++---- testtools/tests/test_runtest.py | 25 +++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/testtools/__init__.py b/testtools/__init__.py index c13947b..06e9eef 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -11,7 +11,7 @@ __all__ = [ 'MultipleExceptions', 'MultiTestResult', 'PlaceHolder', - 'run_tests_with', + 'run_test_with', 'TestCase', 'TestResult', 'TextTestResult', @@ -34,7 +34,7 @@ from testtools.testcase import ( PlaceHolder, TestCase, clone_test_with_new_id, - run_tests_with, + run_test_with, skip, skipIf, skipUnless, diff --git a/testtools/testcase.py b/testtools/testcase.py index 4ba57e6..ebfd337 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -6,7 +6,7 @@ __metaclass__ = type __all__ = [ 'clone_test_with_new_id', 'MultipleExceptions', - 'run_tests_with', + 'run_test_with', 'skip', 'skipIf', 'skipUnless', @@ -62,9 +62,25 @@ except ImportError: """ -def run_tests_with(test_runner): +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_tests_with = test_runner + f._run_test_with = make_test_runner return f return decorator @@ -109,7 +125,7 @@ class TestCase(unittest.TestCase): # TestCase is safe to use with clone_test_with_new_id. self.__details = None test_method = self._get_test_method() - self.__RunTest = getattr(test_method, '_run_tests_with', runTest) + self.__RunTest = getattr(test_method, '_run_test_with', runTest) self.__exception_handlers = [] self.exception_handlers = [ (self.skipException, self._report_skip), diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index ad2a3d9..c3c3d81 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -4,7 +4,7 @@ from testtools import ( ExtendedToOriginalDecorator, - run_tests_with, + run_test_with, RunTest, TestCase, TestResult, @@ -208,8 +208,9 @@ class TestTestCaseSupportForRunTest(TestCase): self.assertThat(from_run_test, Is(CustomRunTest.marker)) def test_decorator_for_run_test(self): + # Individual test methods can be marked as needing a special runner. class SomeCase(TestCase): - @run_tests_with(CustomRunTest) + @run_test_with(CustomRunTest) def test_foo(self): pass result = TestResult() @@ -217,6 +218,26 @@ class TestTestCaseSupportForRunTest(TestCase): 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_suite(): from unittest import TestLoader From a3fd2583d4001481edfcb8df4197a1014b5ce86e Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 17:09:19 +0100 Subject: [PATCH 6/8] Improve manual for RunTest handling. --- MANUAL | 43 ++++++++++++++++++++++++++++++++++--------- testtools/testcase.py | 5 +++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/MANUAL b/MANUAL index 1a43e70..62b2dd4 100644 --- a/MANUAL +++ b/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 ~~~~~~~~~~~~~~~~~~~ diff --git a/testtools/testcase.py b/testtools/testcase.py index ebfd337..b36b850 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -98,8 +98,9 @@ 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 `RunTest` class to run tests with. Defaults to - `RunTest`. + :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 From d9facb542cc4837d41dde244f9152a7d95f8c484 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 17:16:30 +0100 Subject: [PATCH 7/8] Bring the behaviour in line with the documentation. --- testtools/testcase.py | 10 +++++++--- testtools/tests/test_runtest.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index b36b850..018b525 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -112,10 +112,11 @@ class TestCase(unittest.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. """ - runTest = kwargs.pop('runTest', self.run_tests_with) + runTest = kwargs.pop('runTest', None) unittest.TestCase.__init__(self, *args, **kwargs) self._cleanups = [] self._unique_id_gen = itertools.count(1) @@ -126,7 +127,10 @@ class TestCase(unittest.TestCase): # TestCase is safe to use with clone_test_with_new_id. self.__details = None test_method = self._get_test_method() - self.__RunTest = getattr(test_method, '_run_test_with', runTest) + 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), diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index c3c3d81..abc0d6f 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -207,6 +207,22 @@ class TestTestCaseSupportForRunTest(TestCase): 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): @@ -238,6 +254,22 @@ class TestTestCaseSupportForRunTest(TestCase): 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 From e832e8623d58d6fc3959d18f1f1b315fd7a7cf1d Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 17 Oct 2010 17:20:55 +0100 Subject: [PATCH 8/8] News! --- NEWS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS b/NEWS index 2bee113..a024fed 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,12 @@ testtools NEWS NEXT ~~~~ +* 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 ~~~~~