From 07ef487a96b826584f7871629c4cc4754e41242b Mon Sep 17 00:00:00 2001 From: Michael Foord Date: Sat, 2 May 2009 22:43:34 +0000 Subject: [PATCH] --- Doc/library/unittest.rst | 14 +++ Doc/whatsnew/2.7.rst | 4 + Lib/test/test_unittest.py | 236 ++++++++++++++++++++++++++++++++------ Lib/unittest.py | 31 ++++- Misc/NEWS | 11 ++ 5 files changed, 257 insertions(+), 39 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index b8aed9b3e78..e1d416d607a 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1318,6 +1318,20 @@ Loading and running tests The default implementation does nothing. + .. method:: startTestRun(test) + + Called once before any tests are executed. + + .. versionadded:: 2.7 + + + .. method:: stopTestRun(test) + + Called once before any tests are executed. + + .. versionadded:: 2.7 + + .. method:: addError(test, err) Called when the test case *test* raises an unexpected exception *err* is a diff --git a/Doc/whatsnew/2.7.rst b/Doc/whatsnew/2.7.rst index 3243c0adc0d..9182c45349d 100644 --- a/Doc/whatsnew/2.7.rst +++ b/Doc/whatsnew/2.7.rst @@ -517,6 +517,10 @@ changes, or look through the Subversion logs for all the details. If False ``main`` doesn't call :func:`sys.exit` allowing it to be used from the interactive interpreter. :issue:`3379`. + :class:`TestResult` has new :meth:`startTestRun` and + :meth:`stopTestRun` methods; called immediately before + and after a test run. :issue:`5728` by Robert Collins. + * The :func:`is_zipfile` function in the :mod:`zipfile` module will now accept a file object, in addition to the path names accepted in earlier versions. (Contributed by Gabriel Genellina; :issue:`4756`.) diff --git a/Lib/test/test_unittest.py b/Lib/test/test_unittest.py index 13bd78aaf4d..c77cc16f256 100644 --- a/Lib/test/test_unittest.py +++ b/Lib/test/test_unittest.py @@ -6,6 +6,7 @@ Still need testing: TestCase.{assert,fail}* methods (some are tested implicitly) """ +from StringIO import StringIO import re from test import test_support import unittest @@ -26,10 +27,18 @@ class LoggingResult(unittest.TestResult): self._events.append('startTest') super(LoggingResult, self).startTest(test) + def startTestRun(self): + self._events.append('startTestRun') + super(LoggingResult, self).startTestRun() + def stopTest(self, test): self._events.append('stopTest') super(LoggingResult, self).stopTest(test) + def stopTestRun(self): + self._events.append('stopTestRun') + super(LoggingResult, self).stopTestRun() + def addFailure(self, *args): self._events.append('addFailure') super(LoggingResult, self).addFailure(*args) @@ -1817,6 +1826,12 @@ class Test_TestResult(TestCase): self.assertEqual(result.testsRun, 1) self.assertEqual(result.shouldStop, False) + # "Called before and after tests are run. The default implementation does nothing." + def test_startTestRun_stopTestRun(self): + result = unittest.TestResult() + result.startTestRun() + result.stopTestRun() + # "addSuccess(test)" # ... # "Called when the test case test succeeds" @@ -1964,6 +1979,53 @@ class Foo(unittest.TestCase): class Bar(Foo): def test2(self): pass +class LoggingTestCase(unittest.TestCase): + """A test case which logs its calls.""" + + def __init__(self, events): + super(LoggingTestCase, self).__init__('test') + self.events = events + + def setUp(self): + self.events.append('setUp') + + def test(self): + self.events.append('test') + + def tearDown(self): + self.events.append('tearDown') + +class ResultWithNoStartTestRunStopTestRun(object): + """An object honouring TestResult before startTestRun/stopTestRun.""" + + def __init__(self): + self.failures = [] + self.errors = [] + self.testsRun = 0 + self.skipped = [] + self.expectedFailures = [] + self.unexpectedSuccesses = [] + self.shouldStop = False + + def startTest(self, test): + pass + + def stopTest(self, test): + pass + + def addError(self, test): + pass + + def addFailure(self, test): + pass + + def addSuccess(self, test): + pass + + def wasSuccessful(self): + return True + + ################################################################ ### /Support code for Test_TestCase @@ -2058,21 +2120,32 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): events = [] result = LoggingResult(events) - class Foo(unittest.TestCase): + class Foo(LoggingTestCase): def setUp(self): - events.append('setUp') + super(Foo, self).setUp() raise RuntimeError('raised by Foo.setUp') - def test(self): - events.append('test') - - def tearDown(self): - events.append('tearDown') - - Foo('test').run(result) + Foo(events).run(result) expected = ['startTest', 'setUp', 'addError', 'stopTest'] self.assertEqual(events, expected) + # "With a temporary result stopTestRun is called when setUp errors. + def test_run_call_order__error_in_setUp_default_result(self): + events = [] + + class Foo(LoggingTestCase): + def defaultTestResult(self): + return LoggingResult(self.events) + + def setUp(self): + super(Foo, self).setUp() + raise RuntimeError('raised by Foo.setUp') + + Foo(events).run() + expected = ['startTestRun', 'startTest', 'setUp', 'addError', + 'stopTest', 'stopTestRun'] + self.assertEqual(events, expected) + # "When a setUp() method is defined, the test runner will run that method # prior to each test. Likewise, if a tearDown() method is defined, the # test runner will invoke that method after each test. In the example, @@ -2084,20 +2157,32 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): events = [] result = LoggingResult(events) - class Foo(unittest.TestCase): - def setUp(self): - events.append('setUp') - + class Foo(LoggingTestCase): def test(self): - events.append('test') + super(Foo, self).test() raise RuntimeError('raised by Foo.test') - def tearDown(self): - events.append('tearDown') - expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown', 'stopTest'] - Foo('test').run(result) + Foo(events).run(result) + self.assertEqual(events, expected) + + # "With a default result, an error in the test still results in stopTestRun + # being called." + def test_run_call_order__error_in_test_default_result(self): + events = [] + + class Foo(LoggingTestCase): + def defaultTestResult(self): + return LoggingResult(self.events) + + def test(self): + super(Foo, self).test() + raise RuntimeError('raised by Foo.test') + + expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addError', + 'tearDown', 'stopTest', 'stopTestRun'] + Foo(events).run() self.assertEqual(events, expected) # "When a setUp() method is defined, the test runner will run that method @@ -2111,20 +2196,30 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): events = [] result = LoggingResult(events) - class Foo(unittest.TestCase): - def setUp(self): - events.append('setUp') - + class Foo(LoggingTestCase): def test(self): - events.append('test') + super(Foo, self).test() self.fail('raised by Foo.test') - def tearDown(self): - events.append('tearDown') - expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown', 'stopTest'] - Foo('test').run(result) + Foo(events).run(result) + self.assertEqual(events, expected) + + # "When a test fails with a default result stopTestRun is still called." + def test_run_call_order__failure_in_test_default_result(self): + + class Foo(LoggingTestCase): + def defaultTestResult(self): + return LoggingResult(self.events) + def test(self): + super(Foo, self).test() + self.fail('raised by Foo.test') + + expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addFailure', + 'tearDown', 'stopTest', 'stopTestRun'] + events = [] + Foo(events).run() self.assertEqual(events, expected) # "When a setUp() method is defined, the test runner will run that method @@ -2138,22 +2233,44 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): events = [] result = LoggingResult(events) - class Foo(unittest.TestCase): - def setUp(self): - events.append('setUp') - - def test(self): - events.append('test') - + class Foo(LoggingTestCase): def tearDown(self): - events.append('tearDown') + super(Foo, self).tearDown() raise RuntimeError('raised by Foo.tearDown') - Foo('test').run(result) + Foo(events).run(result) expected = ['startTest', 'setUp', 'test', 'tearDown', 'addError', 'stopTest'] self.assertEqual(events, expected) + # "When tearDown errors with a default result stopTestRun is still called." + def test_run_call_order__error_in_tearDown_default_result(self): + + class Foo(LoggingTestCase): + def defaultTestResult(self): + return LoggingResult(self.events) + def tearDown(self): + super(Foo, self).tearDown() + raise RuntimeError('raised by Foo.tearDown') + + events = [] + Foo(events).run() + expected = ['startTestRun', 'startTest', 'setUp', 'test', 'tearDown', + 'addError', 'stopTest', 'stopTestRun'] + self.assertEqual(events, expected) + + # "TestCase.run() still works when the defaultTestResult is a TestResult + # that does not support startTestRun and stopTestRun. + def test_run_call_order_default_result(self): + + class Foo(unittest.TestCase): + def defaultTestResult(self): + return ResultWithNoStartTestRunStopTestRun() + def test(self): + pass + + Foo('test').run() + # "This class attribute gives the exception raised by the test() method. # If a test framework needs to use a specialized exception, possibly to # carry additional information, it must subclass this exception in @@ -2244,7 +2361,9 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): self.failUnless(isinstance(Foo().id(), basestring)) # "If result is omitted or None, a temporary result object is created - # and used, but is not made available to the caller" + # and used, but is not made available to the caller. As TestCase owns the + # temporary result startTestRun and stopTestRun are called. + def test_run__uses_defaultTestResult(self): events = [] @@ -2258,7 +2377,8 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): # Make run() find a result object on its own Foo('test').run() - expected = ['startTest', 'test', 'addSuccess', 'stopTest'] + expected = ['startTestRun', 'startTest', 'test', 'addSuccess', + 'stopTest', 'stopTestRun'] self.assertEqual(events, expected) def testShortDescriptionWithoutDocstring(self): @@ -3215,6 +3335,46 @@ class Test_TestProgram(TestCase): testLoader=self.FooBarLoader()) +class Test_TextTestRunner(TestCase): + """Tests for TextTestRunner.""" + + def test_works_with_result_without_startTestRun_stopTestRun(self): + class OldTextResult(ResultWithNoStartTestRunStopTestRun): + separator2 = '' + def printErrors(self): + pass + + class Runner(unittest.TextTestRunner): + def __init__(self): + super(Runner, self).__init__(StringIO()) + + def _makeResult(self): + return OldTextResult() + + runner = Runner() + runner.run(unittest.TestSuite()) + + def test_startTestRun_stopTestRun_called(self): + class LoggingTextResult(LoggingResult): + separator2 = '' + def printErrors(self): + pass + + class LoggingRunner(unittest.TextTestRunner): + def __init__(self, events): + super(LoggingRunner, self).__init__(StringIO()) + self._events = events + + def _makeResult(self): + return LoggingTextResult(self._events) + + events = [] + runner = LoggingRunner(events) + runner.run(unittest.TestSuite()) + expected = ['startTestRun', 'stopTestRun'] + self.assertEqual(events, expected) + + ###################################################################### ## Main ###################################################################### diff --git a/Lib/unittest.py b/Lib/unittest.py index d5c2927d641..b2dc320fafd 100644 --- a/Lib/unittest.py +++ b/Lib/unittest.py @@ -186,10 +186,22 @@ class TestResult(object): "Called when the given test is about to be run" self.testsRun = self.testsRun + 1 + def startTestRun(self): + """Called once before any tests are executed. + + See startTest for a method called before each test. + """ + def stopTest(self, test): "Called when the given test has been run" pass + def stopTestRun(self): + """Called once after all tests are executed. + + See stopTest for a method called after each test. + """ + def addError(self, test, err): """Called when an error has occurred. 'err' is a tuple of values as returned by sys.exc_info(). @@ -437,8 +449,13 @@ class TestCase(object): (_strclass(self.__class__), self._testMethodName) def run(self, result=None): + orig_result = result if result is None: result = self.defaultTestResult() + startTestRun = getattr(result, 'startTestRun', None) + if startTestRun is not None: + startTestRun() + self._result = result result.startTest(self) testMethod = getattr(self, self._testMethodName) @@ -478,6 +495,10 @@ class TestCase(object): result.addSuccess(self) finally: result.stopTest(self) + if orig_result is None: + stopTestRun = getattr(result, 'stopTestRun', None) + if stopTestRun is not None: + stopTestRun() def doCleanups(self): """Execute all cleanup functions. Normally called for you after @@ -1433,7 +1454,15 @@ class TextTestRunner(object): "Run the given test case or test suite." result = self._makeResult() startTime = time.time() - test(result) + startTestRun = getattr(result, 'startTestRun', None) + if startTestRun is not None: + startTestRun() + try: + test(result) + finally: + stopTestRun = getattr(result, 'stopTestRun', None) + if stopTestRun is not None: + stopTestRun() stopTime = time.time() timeTaken = stopTime - startTime result.printErrors() diff --git a/Misc/NEWS b/Misc/NEWS index e7516bb0116..579dcb49aaa 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -359,6 +359,17 @@ Library - unittest.assertNotEqual() now uses the inequality operator (!=) instead of the equality operator. +- Issue #5679: The methods unittest.TestCase.addCleanup and doCleanups were added. + addCleanup allows you to add cleanup functions that will be called + unconditionally (after setUp if setUp fails, otherwise after tearDown). This + allows for much simpler resource allocation and deallocation during tests. + +- Issue #3379: unittest.main now takes an optional exit argument. If False main + doesn't call sys.exit allowing it to be used from the interactive interpreter. + +- Issue #5728: unittest.TestResult has new startTestRun and stopTestRun methods; + called immediately before and after a test run. + - Issue #5663: better failure messages for unittest asserts. Default assertTrue and assertFalse messages are now useful. TestCase has a longMessage attribute. This defaults to False, but if set to True useful error messages are shown in