]> granicus.if.org Git - python/commitdiff
(no commit message)
authorMichael Foord <fuzzyman@voidspace.org.uk>
Sat, 2 May 2009 22:43:34 +0000 (22:43 +0000)
committerMichael Foord <fuzzyman@voidspace.org.uk>
Sat, 2 May 2009 22:43:34 +0000 (22:43 +0000)
Doc/library/unittest.rst
Doc/whatsnew/2.7.rst
Lib/test/test_unittest.py
Lib/unittest.py
Misc/NEWS

index b8aed9b3e785f60e8b0dc72c84d7ee75e44c8150..e1d416d607a07a5e5929b94037167a842364ea66 100644 (file)
@@ -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
index 3243c0adc0def37eed08589aec4b1223f78ad3ed..9182c45349d7d38f40116d593d853b48b92b2704 100644 (file)
@@ -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`.)
index 13bd78aaf4d0ccf24614e791f9fc3b357ff31995..c77cc16f25638821a38906920f1da8092905b5d2 100644 (file)
@@ -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,19 +2120,30 @@ 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')
+        Foo(events).run(result)
+        expected = ['startTest', 'setUp', 'addError', 'stopTest']
+        self.assertEqual(events, expected)
 
-            def tearDown(self):
-                events.append('tearDown')
+    # "With a temporary result stopTestRun is called when setUp errors.
+    def test_run_call_order__error_in_setUp_default_result(self):
+        events = []
 
-        Foo('test').run(result)
-        expected = ['startTest', 'setUp', 'addError', 'stopTest']
+        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
@@ -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
 ######################################################################
index d5c2927d641ff28513306620d29803cfded3944d..b2dc320fafdaa7368f4aec56ce37a6531e73cbc3 100644 (file)
@@ -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()
index e7516bb01164f3148e5fabfcfd39253042f5b1f4..579dcb49aaa1d16a66580eaff3b4d57364736150 100644 (file)
--- 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