Issue #16997: unittest.TestCase now provides a subTest() context manager to procedurally generate, in an easy way, small test instances.

This commit is contained in:
Antoine Pitrou 2013-03-20 20:16:47 +01:00
parent a612176c9c
commit c9b3ef2df0
10 changed files with 540 additions and 106 deletions

View file

@ -41,7 +41,7 @@ class TestHashing(object):
self.fail("Problem hashing %s and %s: %s" % (obj_1, obj_2, e))
class LoggingResult(unittest.TestResult):
class _BaseLoggingResult(unittest.TestResult):
def __init__(self, log):
self._events = log
super().__init__()
@ -52,7 +52,7 @@ class LoggingResult(unittest.TestResult):
def startTestRun(self):
self._events.append('startTestRun')
super(LoggingResult, self).startTestRun()
super().startTestRun()
def stopTest(self, test):
self._events.append('stopTest')
@ -60,7 +60,7 @@ class LoggingResult(unittest.TestResult):
def stopTestRun(self):
self._events.append('stopTestRun')
super(LoggingResult, self).stopTestRun()
super().stopTestRun()
def addFailure(self, *args):
self._events.append('addFailure')
@ -68,7 +68,7 @@ class LoggingResult(unittest.TestResult):
def addSuccess(self, *args):
self._events.append('addSuccess')
super(LoggingResult, self).addSuccess(*args)
super().addSuccess(*args)
def addError(self, *args):
self._events.append('addError')
@ -76,15 +76,39 @@ class LoggingResult(unittest.TestResult):
def addSkip(self, *args):
self._events.append('addSkip')
super(LoggingResult, self).addSkip(*args)
super().addSkip(*args)
def addExpectedFailure(self, *args):
self._events.append('addExpectedFailure')
super(LoggingResult, self).addExpectedFailure(*args)
super().addExpectedFailure(*args)
def addUnexpectedSuccess(self, *args):
self._events.append('addUnexpectedSuccess')
super(LoggingResult, self).addUnexpectedSuccess(*args)
super().addUnexpectedSuccess(*args)
class LegacyLoggingResult(_BaseLoggingResult):
"""
A legacy TestResult implementation, without an addSubTest method,
which records its method calls.
"""
@property
def addSubTest(self):
raise AttributeError
class LoggingResult(_BaseLoggingResult):
"""
A TestResult implementation which records its method calls.
"""
def addSubTest(self, test, subtest, err):
if err is None:
self._events.append('addSubTestSuccess')
else:
self._events.append('addSubTestFailure')
super().addSubTest(test, subtest, err)
class ResultWithNoStartTestRunStopTestRun(object):

View file

@ -13,7 +13,7 @@ from test import support
import unittest
from .support import (
TestEquality, TestHashing, LoggingResult,
TestEquality, TestHashing, LoggingResult, LegacyLoggingResult,
ResultWithNoStartTestRunStopTestRun
)
@ -297,6 +297,98 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
Foo('test').run()
def _check_call_order__subtests(self, result, events, expected_events):
class Foo(Test.LoggingTestCase):
def test(self):
super(Foo, self).test()
for i in [1, 2, 3]:
with self.subTest(i=i):
if i == 1:
self.fail('failure')
for j in [2, 3]:
with self.subTest(j=j):
if i * j == 6:
raise RuntimeError('raised by Foo.test')
1 / 0
# Order is the following:
# i=1 => subtest failure
# i=2, j=2 => subtest success
# i=2, j=3 => subtest error
# i=3, j=2 => subtest error
# i=3, j=3 => subtest success
# toplevel => error
Foo(events).run(result)
self.assertEqual(events, expected_events)
def test_run_call_order__subtests(self):
events = []
result = LoggingResult(events)
expected = ['startTest', 'setUp', 'test', 'tearDown',
'addSubTestFailure', 'addSubTestSuccess',
'addSubTestFailure', 'addSubTestFailure',
'addSubTestSuccess', 'addError', 'stopTest']
self._check_call_order__subtests(result, events, expected)
def test_run_call_order__subtests_legacy(self):
# With a legacy result object (without a addSubTest method),
# text execution stops after the first subtest failure.
events = []
result = LegacyLoggingResult(events)
expected = ['startTest', 'setUp', 'test', 'tearDown',
'addFailure', 'stopTest']
self._check_call_order__subtests(result, events, expected)
def _check_call_order__subtests_success(self, result, events, expected_events):
class Foo(Test.LoggingTestCase):
def test(self):
super(Foo, self).test()
for i in [1, 2]:
with self.subTest(i=i):
for j in [2, 3]:
with self.subTest(j=j):
pass
Foo(events).run(result)
self.assertEqual(events, expected_events)
def test_run_call_order__subtests_success(self):
events = []
result = LoggingResult(events)
# The 6 subtest successes are individually recorded, in addition
# to the whole test success.
expected = (['startTest', 'setUp', 'test', 'tearDown']
+ 6 * ['addSubTestSuccess']
+ ['addSuccess', 'stopTest'])
self._check_call_order__subtests_success(result, events, expected)
def test_run_call_order__subtests_success_legacy(self):
# With a legacy result, only the whole test success is recorded.
events = []
result = LegacyLoggingResult(events)
expected = ['startTest', 'setUp', 'test', 'tearDown',
'addSuccess', 'stopTest']
self._check_call_order__subtests_success(result, events, expected)
def test_run_call_order__subtests_failfast(self):
events = []
result = LoggingResult(events)
result.failfast = True
class Foo(Test.LoggingTestCase):
def test(self):
super(Foo, self).test()
with self.subTest(i=1):
self.fail('failure')
with self.subTest(i=2):
self.fail('failure')
self.fail('failure')
expected = ['startTest', 'setUp', 'test', 'tearDown',
'addSubTestFailure', 'stopTest']
Foo(events).run(result)
self.assertEqual(events, expected)
# "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

View file

@ -234,6 +234,37 @@ class Test_TestResult(unittest.TestCase):
'testGetDescriptionWithoutDocstring (' + __name__ +
'.Test_TestResult)')
def testGetSubTestDescriptionWithoutDocstring(self):
with self.subTest(foo=1, bar=2):
result = unittest.TextTestResult(None, True, 1)
self.assertEqual(
result.getDescription(self._subtest),
'testGetSubTestDescriptionWithoutDocstring (' + __name__ +
'.Test_TestResult) (bar=2, foo=1)')
with self.subTest('some message'):
result = unittest.TextTestResult(None, True, 1)
self.assertEqual(
result.getDescription(self._subtest),
'testGetSubTestDescriptionWithoutDocstring (' + __name__ +
'.Test_TestResult) [some message]')
def testGetSubTestDescriptionWithoutDocstringAndParams(self):
with self.subTest():
result = unittest.TextTestResult(None, True, 1)
self.assertEqual(
result.getDescription(self._subtest),
'testGetSubTestDescriptionWithoutDocstringAndParams '
'(' + __name__ + '.Test_TestResult) (<subtest>)')
def testGetNestedSubTestDescriptionWithoutDocstring(self):
with self.subTest(foo=1):
with self.subTest(bar=2):
result = unittest.TextTestResult(None, True, 1)
self.assertEqual(
result.getDescription(self._subtest),
'testGetNestedSubTestDescriptionWithoutDocstring '
'(' + __name__ + '.Test_TestResult) (bar=2, foo=1)')
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def testGetDescriptionWithOneLineDocstring(self):
@ -245,6 +276,18 @@ class Test_TestResult(unittest.TestCase):
'(' + __name__ + '.Test_TestResult)\n'
'Tests getDescription() for a method with a docstring.'))
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def testGetSubTestDescriptionWithOneLineDocstring(self):
"""Tests getDescription() for a method with a docstring."""
result = unittest.TextTestResult(None, True, 1)
with self.subTest(foo=1, bar=2):
self.assertEqual(
result.getDescription(self._subtest),
('testGetSubTestDescriptionWithOneLineDocstring '
'(' + __name__ + '.Test_TestResult) (bar=2, foo=1)\n'
'Tests getDescription() for a method with a docstring.'))
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def testGetDescriptionWithMultiLineDocstring(self):
@ -259,6 +302,21 @@ class Test_TestResult(unittest.TestCase):
'Tests getDescription() for a method with a longer '
'docstring.'))
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def testGetSubTestDescriptionWithMultiLineDocstring(self):
"""Tests getDescription() for a method with a longer docstring.
The second line of the docstring.
"""
result = unittest.TextTestResult(None, True, 1)
with self.subTest(foo=1, bar=2):
self.assertEqual(
result.getDescription(self._subtest),
('testGetSubTestDescriptionWithMultiLineDocstring '
'(' + __name__ + '.Test_TestResult) (bar=2, foo=1)\n'
'Tests getDescription() for a method with a longer '
'docstring.'))
def testStackFrameTrimming(self):
class Frame(object):
class tb_frame(object):

View file

@ -5,6 +5,7 @@ import pickle
import subprocess
import unittest
from unittest.case import _Outcome
from .support import LoggingResult, ResultWithNoStartTestRunStopTestRun
@ -42,12 +43,8 @@ class TestCleanUp(unittest.TestCase):
def testNothing(self):
pass
class MockOutcome(object):
success = True
errors = []
test = TestableTest('testNothing')
test._outcomeForDoCleanups = MockOutcome
outcome = test._outcome = _Outcome()
exc1 = Exception('foo')
exc2 = Exception('bar')
@ -61,9 +58,10 @@ class TestCleanUp(unittest.TestCase):
test.addCleanup(cleanup2)
self.assertFalse(test.doCleanups())
self.assertFalse(MockOutcome.success)
self.assertFalse(outcome.success)
(Type1, instance1, _), (Type2, instance2, _) = reversed(MockOutcome.errors)
((_, (Type1, instance1, _)),
(_, (Type2, instance2, _))) = reversed(outcome.errors)
self.assertEqual((Type1, instance1), (Exception, exc1))
self.assertEqual((Type2, instance2), (Exception, exc2))

View file

@ -29,6 +29,31 @@ class Test_TestSkipping(unittest.TestCase):
self.assertEqual(result.skipped, [(test, "testing")])
self.assertEqual(result.testsRun, 1)
def test_skipping_subtests(self):
class Foo(unittest.TestCase):
def test_skip_me(self):
with self.subTest(a=1):
with self.subTest(b=2):
self.skipTest("skip 1")
self.skipTest("skip 2")
self.skipTest("skip 3")
events = []
result = LoggingResult(events)
test = Foo("test_skip_me")
test.run(result)
self.assertEqual(events, ['startTest', 'addSkip', 'addSkip',
'addSkip', 'stopTest'])
self.assertEqual(len(result.skipped), 3)
subtest, msg = result.skipped[0]
self.assertEqual(msg, "skip 1")
self.assertIsInstance(subtest, unittest.TestCase)
self.assertIsNot(subtest, test)
subtest, msg = result.skipped[1]
self.assertEqual(msg, "skip 2")
self.assertIsInstance(subtest, unittest.TestCase)
self.assertIsNot(subtest, test)
self.assertEqual(result.skipped[2], (test, "skip 3"))
def test_skipping_decorators(self):
op_table = ((unittest.skipUnless, False, True),
(unittest.skipIf, True, False))
@ -95,6 +120,31 @@ class Test_TestSkipping(unittest.TestCase):
self.assertEqual(result.expectedFailures[0][0], test)
self.assertTrue(result.wasSuccessful())
def test_expected_failure_subtests(self):
# A failure in any subtest counts as the expected failure of the
# whole test.
class Foo(unittest.TestCase):
@unittest.expectedFailure
def test_die(self):
with self.subTest():
# This one succeeds
pass
with self.subTest():
self.fail("help me!")
with self.subTest():
# This one doesn't get executed
self.fail("shouldn't come here")
events = []
result = LoggingResult(events)
test = Foo("test_die")
test.run(result)
self.assertEqual(events,
['startTest', 'addSubTestSuccess',
'addExpectedFailure', 'stopTest'])
self.assertEqual(len(result.expectedFailures), 1)
self.assertIs(result.expectedFailures[0][0], test)
self.assertTrue(result.wasSuccessful())
def test_unexpected_success(self):
class Foo(unittest.TestCase):
@unittest.expectedFailure
@ -110,6 +160,30 @@ class Test_TestSkipping(unittest.TestCase):
self.assertEqual(result.unexpectedSuccesses, [test])
self.assertTrue(result.wasSuccessful())
def test_unexpected_success_subtests(self):
# Success in all subtests counts as the unexpected success of
# the whole test.
class Foo(unittest.TestCase):
@unittest.expectedFailure
def test_die(self):
with self.subTest():
# This one succeeds
pass
with self.subTest():
# So does this one
pass
events = []
result = LoggingResult(events)
test = Foo("test_die")
test.run(result)
self.assertEqual(events,
['startTest',
'addSubTestSuccess', 'addSubTestSuccess',
'addUnexpectedSuccess', 'stopTest'])
self.assertFalse(result.failures)
self.assertEqual(result.unexpectedSuccesses, [test])
self.assertTrue(result.wasSuccessful())
def test_skip_doesnt_run_setup(self):
class Foo(unittest.TestCase):
wasSetUp = False