Show local variables in tracebacks.
+.. cmdoption:: --durations N
+
+ Show the N slowest test cases (N=0 for all).
+
.. versionadded:: 3.2
The command-line options ``-b``, ``-c`` and ``-f`` were added.
.. versionadded:: 3.7
The command-line option ``-k``.
+.. versionadded:: 3.12
+ The command-line option ``--durations``.
+
The command line can also be used for test discovery, for running all of the
tests in a project or just a subset.
-
.. _unittest-test-discovery:
Test Discovery
A list containing :class:`TestCase` instances that were marked as expected
failures, but succeeded.
+ .. attribute:: collectedDurations
+
+ A list containing 2-tuples of :class:`TestCase` instances and floats
+ representing the elapsed time of each test which was run.
+
+ .. versionadded:: 3.12
+
.. attribute:: shouldStop
Set to ``True`` when the execution of tests should stop by :meth:`stop`.
.. versionadded:: 3.4
+ .. method:: addDuration(test, elapsed)
+
+ Called when the test case finishes. *elapsed* is the time represented in
+ seconds, and it includes the execution of cleanup functions.
+
+ .. versionadded:: 3.12
-.. class:: TextTestResult(stream, descriptions, verbosity)
+.. class:: TextTestResult(stream, descriptions, verbosity, *, durations=None)
A concrete implementation of :class:`TestResult` used by the
- :class:`TextTestRunner`.
+ :class:`TextTestRunner`. Subclasses should accept ``**kwargs`` to ensure
+ compatibility as the interface changes.
.. versionadded:: 3.2
+ .. versionadded:: 3.12
+ Added *durations* keyword argument.
+
+ .. versionchanged:: 3.12
+ Subclasses should accept ``**kwargs`` to ensure compatibility as the
+ interface changes.
.. data:: defaultTestLoader
.. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \
- buffer=False, resultclass=None, warnings=None, *, tb_locals=False)
+ buffer=False, resultclass=None, warnings=None, *, \
+ tb_locals=False, durations=None)
A basic test runner implementation that outputs results to a stream. If *stream*
is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class
*warnings* to ``None``.
.. versionchanged:: 3.2
- Added the ``warnings`` argument.
+ Added the *warnings* parameter.
.. versionchanged:: 3.2
The default stream is set to :data:`sys.stderr` at instantiation time rather
than import time.
.. versionchanged:: 3.5
- Added the tb_locals parameter.
+ Added the *tb_locals* parameter.
+
+ .. versionchanged:: 3.12
+ Added the *durations* parameter.
.. method:: _makeResult()
* The Unicode database has been updated to version 15.0.0. (Contributed by
Benjamin Peterson in :gh:`96734`).
+unittest
+--------
+
+Added ``--durations`` command line option, showing the N slowest test cases::
+
+ python3 -m unittest --durations=3 lib.tests.test_threading
+ .....
+ Slowest test durations
+ ----------------------------------------------------------------------
+ 1.210s test_timeout (Lib.test.test_threading.BarrierTests)
+ 1.003s test_default_timeout (Lib.test.test_threading.BarrierTests)
+ 0.518s test_timeout (Lib.test.test_threading.EventTests)
+
+ (0.000 durations hidden. Use -v to show these durations.)
+ ----------------------------------------------------------------------
+ Ran 158 tests in 9.869s
+
+ OK (skipped=3)
+
+(Contributed by Giampaolo Rodola in :issue:`4080`)
+
uuid
----
def wasSuccessful(self):
return True
+
+
+class BufferedWriter:
+ def __init__(self):
+ self.result = ''
+ self.buffer = ''
+
+ def write(self, arg):
+ self.buffer += arg
+
+ def flush(self):
+ self.result += self.buffer
+ self.buffer = ''
+
+ def getvalue(self):
+ return self.result
self.testRunner = FakeRunner
self.test = test
self.result = None
+ self.durations = None
p = Program(False)
p.runTests()
'verbosity': verbosity,
'failfast': failfast,
'tb_locals': False,
- 'warnings': None})])
+ 'warnings': None,
+ 'durations': None})])
self.assertEqual(FakeRunner.runArgs, [test])
self.assertEqual(p.result, result)
'verbosity': verbosity,
'failfast': failfast,
'tb_locals': False,
- 'warnings': None})])
+ 'warnings': None,
+ 'durations': None})])
self.assertEqual(FakeRunner.runArgs, [test])
self.assertEqual(p.result, result)
program.failfast = 'failfast'
program.buffer = 'buffer'
program.warnings = 'warnings'
+ program.durations = '5'
program.runTests()
'failfast': 'failfast',
'buffer': 'buffer',
'tb_locals': False,
- 'warnings': 'warnings'})
+ 'warnings': 'warnings',
+ 'durations': '5'})
self.assertEqual(FakeRunner.test, 'test')
self.assertIs(program.result, RESULT)
'failfast': False,
'tb_locals': True,
'verbosity': 1,
- 'warnings': None})
+ 'warnings': None,
+ 'durations': None})
def testRunTestsOldRunnerClass(self):
program = self.program
program.failfast = 'failfast'
program.buffer = 'buffer'
program.test = 'test'
+ program.durations = '0'
program.runTests()
program = self.program
program.catchbreak = True
+ program.durations = None
program.testRunner = FakeRunner
import traceback
import unittest
+from unittest import mock
from unittest.util import strclass
+from test.test_unittest.support import BufferedWriter
class MockTraceback(object):
raise ValueError('bad cleanup2')
-class BufferedWriter:
- def __init__(self):
- self.result = ''
- self.buffer = ''
-
- def write(self, arg):
- self.buffer += arg
-
- def flush(self):
- self.result += self.buffer
- self.buffer = ''
-
- def getvalue(self):
- return self.result
-
-
class Test_TestResult(unittest.TestCase):
# Note: there are not separate tests for TestResult.wasSuccessful(),
# TestResult.errors, TestResult.failures, TestResult.testsRun or
import unittest
from unittest.case import _Outcome
-from test.test_unittest.support import (LoggingResult,
- ResultWithNoStartTestRunStopTestRun)
+from test.test_unittest.support import (
+ BufferedWriter,
+ LoggingResult,
+ ResultWithNoStartTestRunStopTestRun,
+)
def resultFactory(*_):
self.assertTrue(runner.descriptions)
self.assertEqual(runner.resultclass, unittest.TextTestResult)
self.assertFalse(runner.tb_locals)
+ self.assertIsNone(runner.durations)
def test_multiple_inheritance(self):
class AResult(unittest.TestResult):
runner = unittest.TextTestRunner(f)
self.assertTrue(runner.stream.stream is f)
+ def test_durations(self):
+ def run(test, expect_durations):
+ stream = BufferedWriter()
+ runner = unittest.TextTestRunner(stream=stream, durations=5, verbosity=2)
+ result = runner.run(test)
+ self.assertEqual(result.durations, 5)
+ stream.flush()
+ text = stream.getvalue()
+ regex = r"\n\d+.\d\d\ds"
+ if expect_durations:
+ self.assertEqual(len(result.collectedDurations), 1)
+ self.assertIn('Slowest test durations', text)
+ self.assertRegex(text, regex)
+ else:
+ self.assertEqual(len(result.collectedDurations), 0)
+ self.assertNotIn('Slowest test durations', text)
+ self.assertNotRegex(text, regex)
+
+ # success
+ class Foo(unittest.TestCase):
+ def test_1(self):
+ pass
+
+ run(Foo('test_1'), True)
+
+ # failure
+ class Foo(unittest.TestCase):
+ def test_1(self):
+ self.assertEqual(0, 1)
+
+ run(Foo('test_1'), True)
+
+ # error
+ class Foo(unittest.TestCase):
+ def test_1(self):
+ 1 / 0
+
+ run(Foo('test_1'), True)
+
+
+ # error in setUp and tearDown
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ 1 / 0
+ tearDown = setUp
+ def test_1(self):
+ pass
+
+ run(Foo('test_1'), True)
+
+ # skip (expect no durations)
+ class Foo(unittest.TestCase):
+ @unittest.skip("reason")
+ def test_1(self):
+ pass
+
+ run(Foo('test_1'), False)
+
+
if __name__ == "__main__":
unittest.main()
import collections
import contextlib
import traceback
+import time
import types
from . import result
else:
addUnexpectedSuccess(self)
+ def _addDuration(self, result, elapsed):
+ try:
+ addDuration = result.addDuration
+ except AttributeError:
+ warnings.warn("TestResult has no addDuration method",
+ RuntimeWarning)
+ else:
+ addDuration(self, elapsed)
+
def _callSetUp(self):
self.setUp()
getattr(testMethod, "__unittest_expecting_failure__", False)
)
outcome = _Outcome(result)
+ start_time = time.perf_counter()
try:
self._outcome = outcome
with outcome.testPartExecutor(self):
self._callTearDown()
self.doCleanups()
+ self._addDuration(result, (time.perf_counter() - start_time))
if outcome.success:
if expecting_failure:
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
- buffer=None, warnings=None, *, tb_locals=False):
+ buffer=None, warnings=None, *, tb_locals=False,
+ durations=None):
if isinstance(module, str):
self.module = __import__(module)
for part in module.split('.')[1:]:
self.verbosity = verbosity
self.buffer = buffer
self.tb_locals = tb_locals
+ self.durations = durations
if warnings is None and not sys.warnoptions:
# even if DeprecationWarnings are ignored by default
# print them anyway unless other warnings settings are
parser.add_argument('--locals', dest='tb_locals',
action='store_true',
help='Show local variables in tracebacks')
+ parser.add_argument('--durations', dest='durations', type=int,
+ default=None, metavar="N",
+ help='Show the N slowest test cases (N=0 for all)')
if self.failfast is None:
parser.add_argument('-f', '--failfast', dest='failfast',
action='store_true',
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings,
- tb_locals=self.tb_locals)
+ tb_locals=self.tb_locals,
+ durations=self.durations)
except TypeError:
- # didn't accept the tb_locals argument
+ # didn't accept the tb_locals or durations argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
self.skipped = []
self.expectedFailures = []
self.unexpectedSuccesses = []
+ self.collectedDurations = []
self.shouldStop = False
self.buffer = False
self.tb_locals = False
"""Called when a test was expected to fail, but succeed."""
self.unexpectedSuccesses.append(test)
+ def addDuration(self, test, elapsed):
+ """Called when a test finished to run, regardless of its outcome."""
+ # support for a TextTestRunner using an old TestResult class
+ if hasattr(self, "collectedDurations"):
+ self.collectedDurations.append((test, elapsed))
+
def wasSuccessful(self):
"""Tells whether or not this result was a success."""
# The hasattr check is for test_result's OldResult test. That
separator1 = '=' * 70
separator2 = '-' * 70
- def __init__(self, stream, descriptions, verbosity):
+ def __init__(self, stream, descriptions, verbosity, *, durations=None):
+ """Construct a TextTestResult. Subclasses should accept **kwargs
+ to ensure compatibility as the interface changes."""
super(TextTestResult, self).__init__(stream, descriptions, verbosity)
self.stream = stream
self.showAll = verbosity > 1
self.dots = verbosity == 1
self.descriptions = descriptions
self._newline = True
+ self.durations = durations
def getDescription(self, test):
doc_first_line = test.shortDescription()
def __init__(self, stream=None, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None, warnings=None,
- *, tb_locals=False):
+ *, tb_locals=False, durations=None):
"""Construct a TextTestRunner.
Subclasses should accept **kwargs to ensure compatibility as the
self.failfast = failfast
self.buffer = buffer
self.tb_locals = tb_locals
+ self.durations = durations
self.warnings = warnings
if resultclass is not None:
self.resultclass = resultclass
def _makeResult(self):
- return self.resultclass(self.stream, self.descriptions, self.verbosity)
+ try:
+ return self.resultclass(self.stream, self.descriptions,
+ self.verbosity, durations=self.durations)
+ except TypeError:
+ # didn't accept the durations argument
+ return self.resultclass(self.stream, self.descriptions,
+ self.verbosity)
+
+ def _printDurations(self, result):
+ if not result.collectedDurations:
+ return
+ ls = sorted(result.collectedDurations, key=lambda x: x[1],
+ reverse=True)
+ if self.durations > 0:
+ ls = ls[:self.durations]
+ self.stream.writeln("Slowest test durations")
+ if hasattr(result, 'separator2'):
+ self.stream.writeln(result.separator2)
+ hidden = False
+ for test, elapsed in ls:
+ if self.verbosity < 2 and elapsed < 0.001:
+ hidden = True
+ continue
+ self.stream.writeln("%-10s %s" % ("%.3fs" % elapsed, test))
+ if hidden:
+ self.stream.writeln("\n(durations < 0.001s were hidden; "
+ "use -v to show these durations)")
+ else:
+ self.stream.writeln("")
def run(self, test):
"Run the given test case or test suite."
stopTime = time.perf_counter()
timeTaken = stopTime - startTime
result.printErrors()
+ if self.durations is not None:
+ self._printDurations(result)
+
if hasattr(result, 'separator2'):
self.stream.writeln(result.separator2)
+
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))
--- /dev/null
+Added ``--durations`` command line option, showing the N slowest test cases.
+:class:`unittest.TextTestRunner` and :class:`unittest.TextTestResult`
+constructors accept a new *durations* keyword argument. Subclasses should take
+this into account or accept ``**kwargs``. Added
+:meth:`unittest.TestResult.addDuration` method and
+:attr:`unittest.TestResult.collectedDurations` attribute.
+
+(Contributed by Giampaolo Rodola)