From 1711c509faf3111bdf5a3a860b2cd01c0dc5d233 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Wed, 26 Oct 2016 23:10:17 +0200 Subject: [PATCH] Fixed #27391 -- Implemented SimpleTestCase.debug(). debug() should bubbled up exceptions if occurring in test, but behave the same as run() when no exceptions occurred. --- django/test/testcases.py | 25 +++++++++- docs/releases/3.1.txt | 4 +- docs/topics/testing/tools.txt | 5 ++ tests/test_utils/test_simpletestcase.py | 61 +++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 468c0c4fbca..ca5712ee8d4 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -9,6 +9,7 @@ from contextlib import contextmanager from copy import copy from difflib import get_close_matches from functools import wraps +from unittest.suite import _DebugResult from unittest.util import safe_repr from urllib.parse import ( parse_qsl, unquote, urlencode, urljoin, urlparse, urlsplit, urlunparse, @@ -235,6 +236,21 @@ class SimpleTestCase(unittest.TestCase): set up. This means that user-defined Test Cases aren't required to include a call to super().setUp(). """ + self._setup_and_call(result) + + def debug(self): + """Perform the same as __call__(), without catching the exception.""" + debug_result = _DebugResult() + self._setup_and_call(debug_result, debug=True) + + def _setup_and_call(self, result, debug=False): + """ + Perform the following in order: pre-setup, run test, post-teardown, + skipping pre/post hooks if test is set to be skipped. + + If debug=True, reraise any errors in setup and use super().debug() + instead of __call__() to run the test. + """ testMethod = getattr(self, self._testMethodName) skipped = ( getattr(self.__class__, "__unittest_skip__", False) or @@ -245,13 +261,20 @@ class SimpleTestCase(unittest.TestCase): try: self._pre_setup() except Exception: + if debug: + raise result.addError(self, sys.exc_info()) return - super().__call__(result) + if debug: + super().debug() + else: + super().__call__(result) if not skipped: try: self._post_teardown() except Exception: + if debug: + raise result.addError(self, sys.exc_info()) return diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index a42d71c855f..a8aaa8e4f9d 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -198,7 +198,9 @@ Templates Tests ~~~~~ -* ... +* :class:`~django.test.SimpleTestCase` now implements the ``debug()`` method to + allow running a test without collecting the result and catching exceptions. + This can be used to support running tests under a debugger. URLs ~~~~ diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 41652cbee1a..13ae83dc809 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -775,6 +775,11 @@ If your tests make any database queries, use subclasses :exc:`unittest.SkipTest` in ``setUpClass()``, be sure to do it before calling ``super()`` to avoid this. +.. versionchanged:: 3.1 + + The ``debug()`` method was implemented to allow running a test without + collecting the result and catching exceptions. + ``TransactionTestCase`` ----------------------- diff --git a/tests/test_utils/test_simpletestcase.py b/tests/test_utils/test_simpletestcase.py index eb1e0017d22..76ec37e80f4 100644 --- a/tests/test_utils/test_simpletestcase.py +++ b/tests/test_utils/test_simpletestcase.py @@ -25,6 +25,11 @@ class DebugInvocationTests(SimpleTestCase): def get_runner(self): return unittest.TextTestRunner(stream=StringIO()) + def isolate_debug_test(self, test_suite, result): + # Suite teardown needs to be manually called to isolate failures. + test_suite._tearDownPreviousClass(None, result) + test_suite._handleModuleTearDown(result) + def test_run_cleanup(self, _pre_setup, _post_teardown): """Simple test run: catches errors and runs cleanup.""" test_suite = unittest.TestSuite() @@ -76,6 +81,58 @@ class DebugInvocationTests(SimpleTestCase): self.assertFalse(_post_teardown.called) self.assertFalse(_pre_setup.called) + def test_debug_cleanup(self, _pre_setup, _post_teardown): + """Simple debug run without errors.""" + test_suite = unittest.TestSuite() + test_suite.addTest(ErrorTestCase('simple_test')) + test_suite.debug() + _pre_setup.assert_called_once_with() + _post_teardown.assert_called_once_with() + + def test_debug_bubbles_error(self, _pre_setup, _post_teardown): + """debug() bubbles up exceptions before cleanup.""" + test_suite = unittest.TestSuite() + test_suite.addTest(ErrorTestCase('raising_test')) + msg = 'debug() bubbles up exceptions before cleanup.' + with self.assertRaisesMessage(Exception, msg): + # This is the same as test_suite.debug(). + result = _DebugResult() + test_suite.run(result, debug=True) + # pre-setup is called but not post-teardown. + _pre_setup.assert_called_once_with() + self.assertFalse(_post_teardown.called) + self.isolate_debug_test(test_suite, result) + + def test_debug_bubbles_pre_setup_error(self, _pre_setup, _post_teardown): + """debug() bubbles up exceptions during _pre_setup.""" + msg = 'Exception in _pre_setup.' + _pre_setup.side_effect = Exception(msg) + test_suite = unittest.TestSuite() + test_suite.addTest(ErrorTestCase('simple_test')) + with self.assertRaisesMessage(Exception, msg): + # This is the same as test_suite.debug(). + result = _DebugResult() + test_suite.run(result, debug=True) + # pre-setup is called but not post-teardown. + _pre_setup.assert_called_once_with() + self.assertFalse(_post_teardown.called) + self.isolate_debug_test(test_suite, result) + + def test_debug_bubbles_post_teardown_error(self, _pre_setup, _post_teardown): + """debug() bubbles up exceptions during _post_teardown.""" + msg = 'Exception in _post_teardown.' + _post_teardown.side_effect = Exception(msg) + test_suite = unittest.TestSuite() + test_suite.addTest(ErrorTestCase('simple_test')) + with self.assertRaisesMessage(Exception, msg): + # This is the same as test_suite.debug(). + result = _DebugResult() + test_suite.run(result, debug=True) + # pre-setup and post-teardwn are called. + _pre_setup.assert_called_once_with() + _post_teardown.assert_called_once_with() + self.isolate_debug_test(test_suite, result) + def test_debug_skipped_test_no_cleanup(self, _pre_setup, _post_teardown): test_suite = unittest.TestSuite() test_suite.addTest(ErrorTestCase('skipped_test')) @@ -85,6 +142,4 @@ class DebugInvocationTests(SimpleTestCase): test_suite.run(result, debug=True) self.assertFalse(_post_teardown.called) self.assertFalse(_pre_setup.called) - # Suite teardown needs to be manually called to isolate failure. - test_suite._tearDownPreviousClass(None, result) - test_suite._handleModuleTearDown(result) + self.isolate_debug_test(test_suite, result)