Add addCleanup and doCleanups to unittest.TestCase.

Closes issue 5679.

Michael Foord
This commit is contained in:
Michael Foord 2009-05-02 20:15:05 +00:00
parent 420d4eb1f3
commit e2fb98f467
4 changed files with 195 additions and 24 deletions

View File

@ -978,6 +978,36 @@ Test cases
.. versionadded:: 2.7 .. versionadded:: 2.7
.. method:: addCleanup(function[, *args[, **kwargs]])
Add a function to be called after :meth:`tearDown` to cleanup resources
used during the test. Functions will be called in reverse order to the
order they are added (LIFO). They are called with any arguments and
keyword arguments passed into :meth:`addCleanup` when they are
added.
If :meth:`setUp` fails, meaning that :meth:`tearDown` is not called,
then any cleanup functions added will still be called.
.. versionadded:: 2.7
.. method:: doCleanups()
This method is called uncoditionally after :meth:`tearDown`, or
after :meth:`setUp` if :meth:`setUp` raises an exception.
It is responsible for calling all the cleanup functions added by
:meth:`addCleanup`. If you need cleanup functions to be called
*prior* to :meth:`tearDown` then you can call :meth:`doCleanups`
yourself.
:meth:`doCleanups` pops methods off the stack of cleanup
functions one at a time, so it can be called at any time.
.. versionadded:: 2.7
.. class:: FunctionTestCase(testFunc[, setUp[, tearDown[, description]]]) .. class:: FunctionTestCase(testFunc[, setUp[, tearDown[, description]]])
This class implements the portion of the :class:`TestCase` interface which This class implements the portion of the :class:`TestCase` interface which

View File

@ -452,6 +452,13 @@ changes, or look through the Subversion logs for all the details.
(Implemented by Antoine Pitrou; :issue:`4444`.) (Implemented by Antoine Pitrou; :issue:`4444`.)
The methods :meth:`addCleanup` and :meth:`doCleanups` were added.
:meth:`addCleanup` allows you to add cleanup functions that
will be called unconditionally (after :meth:`setUp` if
:meth:`setUp` fails, otherwise after :meth:`tearDown`). This allows
for much simpler resource allocation and deallocation during tests.
:issue:`5679`
A number of new methods were added that provide more specialized A number of new methods were added that provide more specialized
tests. Many of these methods were written by Google engineers tests. Many of these methods were written by Google engineers
for use in their test suites; Gregory P. Smith, Michael Foord, and for use in their test suites; Gregory P. Smith, Michael Foord, and

View File

@ -3041,6 +3041,111 @@ class TestLongMessage(TestCase):
"^unexpectedly identical: None : oops$"]) "^unexpectedly identical: None : oops$"])
class TestCleanUp(TestCase):
def testCleanUp(self):
class TestableTest(TestCase):
def testNothing(self):
pass
test = TestableTest('testNothing')
self.assertEqual(test._cleanups, [])
cleanups = []
def cleanup1(*args, **kwargs):
cleanups.append((1, args, kwargs))
def cleanup2(*args, **kwargs):
cleanups.append((2, args, kwargs))
test.addCleanup(cleanup1, 1, 2, 3, four='hello', five='goodbye')
test.addCleanup(cleanup2)
self.assertEqual(test._cleanups,
[(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')),
(cleanup2, (), {})])
result = test.doCleanups()
self.assertTrue(result)
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])
def testCleanUpWithErrors(self):
class TestableTest(TestCase):
def testNothing(self):
pass
class MockResult(object):
errors = []
def addError(self, test, exc_info):
self.errors.append((test, exc_info))
result = MockResult()
test = TestableTest('testNothing')
test._result = result
exc1 = Exception('foo')
exc2 = Exception('bar')
def cleanup1():
raise exc1
def cleanup2():
raise exc2
test.addCleanup(cleanup1)
test.addCleanup(cleanup2)
self.assertFalse(test.doCleanups())
(test1, (Type1, instance1, _)), (test2, (Type2, instance2, _)) = reversed(MockResult.errors)
self.assertEqual((test1, Type1, instance1), (test, Exception, exc1))
self.assertEqual((test2, Type2, instance2), (test, Exception, exc2))
def testCleanupInRun(self):
blowUp = False
ordering = []
class TestableTest(TestCase):
def setUp(self):
ordering.append('setUp')
if blowUp:
raise Exception('foo')
def testNothing(self):
ordering.append('test')
def tearDown(self):
ordering.append('tearDown')
test = TestableTest('testNothing')
def cleanup1():
ordering.append('cleanup1')
def cleanup2():
ordering.append('cleanup2')
test.addCleanup(cleanup1)
test.addCleanup(cleanup2)
def success(some_test):
self.assertEqual(some_test, test)
ordering.append('success')
result = unittest.TestResult()
result.addSuccess = success
test.run(result)
self.assertEqual(ordering, ['setUp', 'test', 'tearDown',
'cleanup2', 'cleanup1', 'success'])
blowUp = True
ordering = []
test = TestableTest('testNothing')
test.addCleanup(cleanup1)
test.run(result)
self.assertEqual(ordering, ['setUp', 'cleanup1'])
class Test_TestProgram(TestCase): class Test_TestProgram(TestCase):
# Horrible white box test # Horrible white box test
@ -3110,7 +3215,6 @@ class Test_TestProgram(TestCase):
testLoader=self.FooBarLoader()) testLoader=self.FooBarLoader())
###################################################################### ######################################################################
## Main ## Main
###################################################################### ######################################################################
@ -3119,7 +3223,7 @@ def test_main():
test_support.run_unittest(Test_TestCase, Test_TestLoader, test_support.run_unittest(Test_TestCase, Test_TestLoader,
Test_TestSuite, Test_TestResult, Test_FunctionTestCase, Test_TestSuite, Test_TestResult, Test_FunctionTestCase,
Test_TestSkipping, Test_Assertions, TestLongMessage, Test_TestSkipping, Test_Assertions, TestLongMessage,
Test_TestProgram) Test_TestProgram, TestCleanUp)
if __name__ == "__main__": if __name__ == "__main__":
test_main() test_main()

View File

@ -340,12 +340,14 @@ class TestCase(object):
not have a method with the specified name. not have a method with the specified name.
""" """
self._testMethodName = methodName self._testMethodName = methodName
self._result = None
try: try:
testMethod = getattr(self, methodName) testMethod = getattr(self, methodName)
except AttributeError: except AttributeError:
raise ValueError("no such test method in %s: %s" % \ raise ValueError("no such test method in %s: %s" % \
(self.__class__, methodName)) (self.__class__, methodName))
self._testMethodDoc = testMethod.__doc__ self._testMethodDoc = testMethod.__doc__
self._cleanups = []
# Map types to custom assertEqual functions that will compare # Map types to custom assertEqual functions that will compare
# instances of said type in more detail to generate a more useful # instances of said type in more detail to generate a more useful
@ -372,6 +374,14 @@ class TestCase(object):
""" """
self._type_equality_funcs[typeobj] = _AssertWrapper(function) self._type_equality_funcs[typeobj] = _AssertWrapper(function)
def addCleanup(self, function, *args, **kwargs):
"""Add a function, with arguments, to be called when the test is
completed. Functions added are called on a LIFO basis and are
called after tearDown on test failure or success.
Cleanup items are called even if setUp fails (unlike tearDown)."""
self._cleanups.append((function, args, kwargs))
def setUp(self): def setUp(self):
"Hook method for setting up the test fixture before exercising it." "Hook method for setting up the test fixture before exercising it."
pass pass
@ -429,44 +439,60 @@ class TestCase(object):
def run(self, result=None): def run(self, result=None):
if result is None: if result is None:
result = self.defaultTestResult() result = self.defaultTestResult()
self._result = result
result.startTest(self) result.startTest(self)
testMethod = getattr(self, self._testMethodName) testMethod = getattr(self, self._testMethodName)
try: try:
try:
self.setUp()
except SkipTest as e:
result.addSkip(self, str(e))
return
except Exception:
result.addError(self, sys.exc_info())
return
success = False success = False
try: try:
testMethod() self.setUp()
except self.failureException:
result.addFailure(self, sys.exc_info())
except _ExpectedFailure as e:
result.addExpectedFailure(self, e.exc_info)
except _UnexpectedSuccess:
result.addUnexpectedSuccess(self)
except SkipTest as e: except SkipTest as e:
result.addSkip(self, str(e)) result.addSkip(self, str(e))
except Exception: except Exception:
result.addError(self, sys.exc_info()) result.addError(self, sys.exc_info())
else: else:
success = True try:
testMethod()
except self.failureException:
result.addFailure(self, sys.exc_info())
except _ExpectedFailure as e:
result.addExpectedFailure(self, e.exc_info)
except _UnexpectedSuccess:
result.addUnexpectedSuccess(self)
except SkipTest as e:
result.addSkip(self, str(e))
except Exception:
result.addError(self, sys.exc_info())
else:
success = True
try: try:
self.tearDown() self.tearDown()
except Exception: except Exception:
result.addError(self, sys.exc_info()) result.addError(self, sys.exc_info())
success = False success = False
cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success: if success:
result.addSuccess(self) result.addSuccess(self)
finally: finally:
result.stopTest(self) result.stopTest(self)
def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after
tearDown."""
result = self._result
ok = True
while self._cleanups:
function, args, kwargs = self._cleanups.pop(-1)
try:
function(*args, **kwargs)
except Exception:
ok = False
result.addError(self, sys.exc_info())
return ok
def __call__(self, *args, **kwds): def __call__(self, *args, **kwds):
return self.run(*args, **kwds) return self.run(*args, **kwds)
@ -1538,5 +1564,9 @@ Examples:
main = TestProgram main = TestProgram
##############################################################################
# Executing this module from the command line
##############################################################################
if __name__ == "__main__": if __name__ == "__main__":
main(module=None) main(module=None)