Issue 10611. Issue 9857. Improve the way exception handling, including test skipping, is done inside TestCase.run

This commit is contained in:
Michael Foord 2010-12-19 03:19:47 +00:00
parent addc6f5a21
commit b3468f79ef
6 changed files with 185 additions and 74 deletions

View file

@ -25,7 +25,6 @@ class SkipTest(Exception):
Usually you can use TestResult.skip() or one of the skipping decorators Usually you can use TestResult.skip() or one of the skipping decorators
instead of raising this directly. instead of raising this directly.
""" """
pass
class _ExpectedFailure(Exception): class _ExpectedFailure(Exception):
""" """
@ -42,7 +41,17 @@ class _UnexpectedSuccess(Exception):
""" """
The test was supposed to fail, but it didn't! The test was supposed to fail, but it didn't!
""" """
pass
class _Outcome(object):
def __init__(self):
self.success = True
self.skipped = None
self.unexpectedSuccess = None
self.expectedFailure = None
self.errors = []
self.failures = []
def _id(obj): def _id(obj):
return obj return obj
@ -263,7 +272,7 @@ 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._resultForDoCleanups = None self._outcomeForDoCleanups = None
try: try:
testMethod = getattr(self, methodName) testMethod = getattr(self, methodName)
except AttributeError: except AttributeError:
@ -367,6 +376,36 @@ class TestCase(object):
RuntimeWarning, 2) RuntimeWarning, 2)
result.addSuccess(self) result.addSuccess(self)
def _executeTestPart(self, function, outcome, isTest=False):
try:
function()
except KeyboardInterrupt:
raise
except SkipTest as e:
outcome.success = False
outcome.skipped = str(e)
except _UnexpectedSuccess:
exc_info = sys.exc_info()
outcome.success = False
if isTest:
outcome.unexpectedSuccess = exc_info
else:
outcome.errors.append(exc_info)
except _ExpectedFailure:
outcome.success = False
exc_info = sys.exc_info()
if isTest:
outcome.expectedFailure = exc_info
else:
outcome.errors.append(exc_info)
except self.failureException:
outcome.success = False
outcome.failures.append(sys.exc_info())
exc_info = sys.exc_info()
except:
outcome.success = False
outcome.errors.append(sys.exc_info())
def run(self, result=None): def run(self, result=None):
orig_result = result orig_result = result
if result is None: if result is None:
@ -375,7 +414,6 @@ class TestCase(object):
if startTestRun is not None: if startTestRun is not None:
startTestRun() startTestRun()
self._resultForDoCleanups = result
result.startTest(self) result.startTest(self)
testMethod = getattr(self, self._testMethodName) testMethod = getattr(self, self._testMethodName)
@ -390,51 +428,42 @@ class TestCase(object):
result.stopTest(self) result.stopTest(self)
return return
try: try:
success = False outcome = _Outcome()
try: self._outcomeForDoCleanups = outcome
self.setUp()
except SkipTest as e: self._executeTestPart(self.setUp, outcome)
self._addSkip(result, str(e)) if outcome.success:
except Exception: self._executeTestPart(testMethod, outcome, isTest=True)
result.addError(self, sys.exc_info()) self._executeTestPart(self.tearDown, outcome)
self.doCleanups()
if outcome.success:
result.addSuccess(self)
else: else:
try: if outcome.skipped is not None:
testMethod() self._addSkip(result, outcome.skipped)
except self.failureException: for exc_info in outcome.errors:
result.addFailure(self, sys.exc_info()) result.addError(self, exc_info)
except _ExpectedFailure as e: for exc_info in outcome.failures:
addExpectedFailure = getattr(result, 'addExpectedFailure', None) result.addFailure(self, exc_info)
if addExpectedFailure is not None: if outcome.unexpectedSuccess is not None:
addExpectedFailure(self, e.exc_info)
else:
warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning)
result.addSuccess(self)
except _UnexpectedSuccess:
addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None) addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
if addUnexpectedSuccess is not None: if addUnexpectedSuccess is not None:
addUnexpectedSuccess(self) addUnexpectedSuccess(self)
else: else:
warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures", warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
RuntimeWarning) RuntimeWarning)
result.addFailure(self, sys.exc_info()) result.addFailure(self, outcome.unexpectedSuccess)
except SkipTest as e:
self._addSkip(result, str(e))
except Exception:
result.addError(self, sys.exc_info())
else:
success = True
try: if outcome.expectedFailure is not None:
self.tearDown() addExpectedFailure = getattr(result, 'addExpectedFailure', None)
except Exception: if addExpectedFailure is not None:
result.addError(self, sys.exc_info()) addExpectedFailure(self, outcome.expectedFailure)
success = False else:
warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning)
result.addSuccess(self)
cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success:
result.addSuccess(self)
finally: finally:
result.stopTest(self) result.stopTest(self)
if orig_result is None: if orig_result is None:
@ -445,16 +474,15 @@ class TestCase(object):
def doCleanups(self): def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after """Execute all cleanup functions. Normally called for you after
tearDown.""" tearDown."""
result = self._resultForDoCleanups outcome = self._outcomeForDoCleanups or _Outcome()
ok = True
while self._cleanups: while self._cleanups:
function, args, kwargs = self._cleanups.pop(-1) function, args, kwargs = self._cleanups.pop()
try: part = lambda: function(*args, **kwargs)
function(*args, **kwargs) self._executeTestPart(part, outcome)
except Exception:
ok = False # return this for backwards compatibility
result.addError(self, sys.exc_info()) # even though we no longer us it internally
return ok return outcome.success
def __call__(self, *args, **kwds): def __call__(self, *args, **kwds):
return self.run(*args, **kwds) return self.run(*args, **kwds)

View file

@ -177,8 +177,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
raise RuntimeError('raised by Foo.test') raise RuntimeError('raised by Foo.test')
expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addError', 'stopTest']
Foo(events).run(result) Foo(events).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
@ -195,8 +195,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
raise RuntimeError('raised by Foo.test') raise RuntimeError('raised by Foo.test')
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addError', expected = ['startTestRun', 'startTest', 'setUp', 'test',
'tearDown', 'stopTest', 'stopTestRun'] 'tearDown', 'addError', 'stopTest', 'stopTestRun']
Foo(events).run() Foo(events).run()
self.assertEqual(events, expected) self.assertEqual(events, expected)
@ -216,8 +216,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
self.fail('raised by Foo.test') self.fail('raised by Foo.test')
expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addFailure', 'stopTest']
Foo(events).run(result) Foo(events).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
@ -231,8 +231,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
self.fail('raised by Foo.test') self.fail('raised by Foo.test')
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addFailure', expected = ['startTestRun', 'startTest', 'setUp', 'test',
'tearDown', 'stopTest', 'stopTestRun'] 'tearDown', 'addFailure', 'stopTest', 'stopTestRun']
events = [] events = []
Foo(events).run() Foo(events).run()
self.assertEqual(events, expected) self.assertEqual(events, expected)
@ -1126,3 +1126,82 @@ test case
# exercise the TestCase instance in a way that will invoke # exercise the TestCase instance in a way that will invoke
# the type equality lookup mechanism # the type equality lookup mechanism
unpickled_test.assertEqual(set(), set()) unpickled_test.assertEqual(set(), set())
def testKeyboardInterrupt(self):
def _raise(self=None):
raise KeyboardInterrupt
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _raise
class Test2(unittest.TestCase):
setUp = _raise
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _raise
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_raise)
for klass in (Test1, Test2, Test3, Test4):
with self.assertRaises(KeyboardInterrupt):
klass('test_something').run()
def testSkippingEverywhere(self):
def _skip(self=None):
raise unittest.SkipTest('some reason')
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _skip
class Test2(unittest.TestCase):
setUp = _skip
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _skip
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_skip)
for klass in (Test1, Test2, Test3, Test4):
result = unittest.TestResult()
klass('test_something').run(result)
self.assertEqual(len(result.skipped), 1)
self.assertEqual(result.testsRun, 1)
def testSystemExit(self):
def _raise(self=None):
raise SystemExit
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _raise
class Test2(unittest.TestCase):
setUp = _raise
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _raise
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_raise)
for klass in (Test1, Test2, Test3, Test4):
result = unittest.TestResult()
klass('test_something').run(result)
self.assertEqual(len(result.errors), 1)
self.assertEqual(result.testsRun, 1)

View file

@ -58,8 +58,8 @@ class Test_FunctionTestCase(unittest.TestCase):
def tearDown(): def tearDown():
events.append('tearDown') events.append('tearDown')
expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addError', 'stopTest']
unittest.FunctionTestCase(test, setUp, tearDown).run(result) unittest.FunctionTestCase(test, setUp, tearDown).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
@ -84,8 +84,8 @@ class Test_FunctionTestCase(unittest.TestCase):
def tearDown(): def tearDown():
events.append('tearDown') events.append('tearDown')
expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addFailure', 'stopTest']
unittest.FunctionTestCase(test, setUp, tearDown).run(result) unittest.FunctionTestCase(test, setUp, tearDown).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)

View file

@ -34,9 +34,7 @@ class TestCleanUp(unittest.TestCase):
[(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')), [(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')),
(cleanup2, (), {})]) (cleanup2, (), {})])
result = test.doCleanups() self.assertTrue(test.doCleanups())
self.assertTrue(result)
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))]) self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])
def testCleanUpWithErrors(self): def testCleanUpWithErrors(self):
@ -44,14 +42,12 @@ class TestCleanUp(unittest.TestCase):
def testNothing(self): def testNothing(self):
pass pass
class MockResult(object): class MockOutcome(object):
success = True
errors = [] errors = []
def addError(self, test, exc_info):
self.errors.append((test, exc_info))
result = MockResult()
test = TestableTest('testNothing') test = TestableTest('testNothing')
test._resultForDoCleanups = result test._outcomeForDoCleanups = MockOutcome
exc1 = Exception('foo') exc1 = Exception('foo')
exc2 = Exception('bar') exc2 = Exception('bar')
@ -65,10 +61,11 @@ class TestCleanUp(unittest.TestCase):
test.addCleanup(cleanup2) test.addCleanup(cleanup2)
self.assertFalse(test.doCleanups()) self.assertFalse(test.doCleanups())
self.assertFalse(MockOutcome.success)
(test1, (Type1, instance1, _)), (test2, (Type2, instance2, _)) = reversed(MockResult.errors) (Type1, instance1, _), (Type2, instance2, _) = reversed(MockOutcome.errors)
self.assertEqual((test1, Type1, instance1), (test, Exception, exc1)) self.assertEqual((Type1, instance1), (Exception, exc1))
self.assertEqual((test2, Type2, instance2), (test, Exception, exc2)) self.assertEqual((Type2, instance2), (Exception, exc2))
def testCleanupInRun(self): def testCleanupInRun(self):
blowUp = False blowUp = False

View file

@ -23,6 +23,11 @@ Core and Builtins
Library Library
------- -------
- Issue #10611: SystemExit exception will no longer kill a unittest run.
- Issue #9857: It is now possible to skip a test in a setUp, tearDown or clean
up function.
- Issue #10573: use actual/expected consistently in unittest methods. - Issue #10573: use actual/expected consistently in unittest methods.
The order of the args of assertCountEqual is also changed. The order of the args of assertCountEqual is also changed.
@ -322,7 +327,7 @@ Library
- configparser: the SafeConfigParser class has been renamed to ConfigParser. - configparser: the SafeConfigParser class has been renamed to ConfigParser.
The legacy ConfigParser class has been removed but its interpolation mechanism The legacy ConfigParser class has been removed but its interpolation mechanism
is still available as LegacyInterpolation. is still available as LegacyInterpolation.
- configparser: Usage of RawConfigParser is now discouraged for new projects - configparser: Usage of RawConfigParser is now discouraged for new projects
in favor of ConfigParser(interpolation=None). in favor of ConfigParser(interpolation=None).

View file

@ -5,8 +5,10 @@
################################################################## ##################################################################
[project attributes] [project attributes]
proj.directory-list = [{'dirloc': loc('..'), proj.directory-list = [{'dirloc': loc('..'),
'excludes': [u'Lib/__pycache__', 'excludes': [u'Lib/unittest/test/__pycache__',
u'Lib/__pycache__',
u'Doc/build', u'Doc/build',
u'Lib/unittest/__pycache__',
u'build'], u'build'],
'filter': '*', 'filter': '*',
'include_hidden': False, 'include_hidden': False,