Skip to content

Commit 7e7503c

Browse files
unittest: report class cleanup exceptions (#12250)
Fixes #11728 --------- Co-authored-by: Bruno Oliveira <[email protected]>
1 parent d208c1d commit 7e7503c

File tree

4 files changed

+110
-0
lines changed

4 files changed

+110
-0
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Cyrus Maden
101101
Damian Skrzypczak
102102
Daniel Grana
103103
Daniel Hahler
104+
Daniel Miller
104105
Daniel Nuri
105106
Daniel Sánchez Castelló
106107
Daniel Valenzuela Zenteno

changelog/11728.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.

src/_pytest/unittest.py

+19
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
import pytest
3333

3434

35+
if sys.version_info[:2] < (3, 11):
36+
from exceptiongroup import ExceptionGroup
37+
3538
if TYPE_CHECKING:
3639
import unittest
3740

@@ -111,6 +114,20 @@ def _register_unittest_setup_class_fixture(self, cls: type) -> None:
111114
return None
112115
cleanup = getattr(cls, "doClassCleanups", lambda: None)
113116

117+
def process_teardown_exceptions() -> None:
118+
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
119+
# teardown for the class.
120+
exc_infos = getattr(cls, "tearDown_exceptions", None)
121+
if not exc_infos:
122+
return
123+
exceptions = [exc for (_, exc, _) in exc_infos]
124+
# If a single exception, raise it directly as this provides a more readable
125+
# error (hopefully this will improve in #12255).
126+
if len(exceptions) == 1:
127+
raise exceptions[0]
128+
else:
129+
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
130+
114131
def unittest_setup_class_fixture(
115132
request: FixtureRequest,
116133
) -> Generator[None, None, None]:
@@ -125,13 +142,15 @@ def unittest_setup_class_fixture(
125142
# follow this here.
126143
except Exception:
127144
cleanup()
145+
process_teardown_exceptions()
128146
raise
129147
yield
130148
try:
131149
if teardown is not None:
132150
teardown()
133151
finally:
134152
cleanup()
153+
process_teardown_exceptions()
135154

136155
self.session._fixturemanager._register_fixture(
137156
# Use a unique name to speed up lookup.

testing/test_unittest.py

+89
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,95 @@ def test_cleanup_called_the_right_number_of_times():
15001500
assert passed == 1
15011501

15021502

1503+
class TestClassCleanupErrors:
1504+
"""
1505+
Make sure to show exceptions raised during class cleanup function (those registered
1506+
via addClassCleanup()).
1507+
1508+
See #11728.
1509+
"""
1510+
1511+
def test_class_cleanups_failure_in_setup(self, pytester: Pytester) -> None:
1512+
testpath = pytester.makepyfile(
1513+
"""
1514+
import unittest
1515+
class MyTestCase(unittest.TestCase):
1516+
@classmethod
1517+
def setUpClass(cls):
1518+
def cleanup(n):
1519+
raise Exception(f"fail {n}")
1520+
cls.addClassCleanup(cleanup, 2)
1521+
cls.addClassCleanup(cleanup, 1)
1522+
raise Exception("fail 0")
1523+
def test(self):
1524+
pass
1525+
"""
1526+
)
1527+
result = pytester.runpytest("-s", testpath)
1528+
result.assert_outcomes(passed=0, errors=1)
1529+
result.stdout.fnmatch_lines(
1530+
[
1531+
"*Unittest class cleanup errors *2 sub-exceptions*",
1532+
"*Exception: fail 1",
1533+
"*Exception: fail 2",
1534+
]
1535+
)
1536+
result.stdout.fnmatch_lines(
1537+
[
1538+
"* ERROR at setup of MyTestCase.test *",
1539+
"E * Exception: fail 0",
1540+
]
1541+
)
1542+
1543+
def test_class_cleanups_failure_in_teardown(self, pytester: Pytester) -> None:
1544+
testpath = pytester.makepyfile(
1545+
"""
1546+
import unittest
1547+
class MyTestCase(unittest.TestCase):
1548+
@classmethod
1549+
def setUpClass(cls):
1550+
def cleanup(n):
1551+
raise Exception(f"fail {n}")
1552+
cls.addClassCleanup(cleanup, 2)
1553+
cls.addClassCleanup(cleanup, 1)
1554+
def test(self):
1555+
pass
1556+
"""
1557+
)
1558+
result = pytester.runpytest("-s", testpath)
1559+
result.assert_outcomes(passed=1, errors=1)
1560+
result.stdout.fnmatch_lines(
1561+
[
1562+
"*Unittest class cleanup errors *2 sub-exceptions*",
1563+
"*Exception: fail 1",
1564+
"*Exception: fail 2",
1565+
]
1566+
)
1567+
1568+
def test_class_cleanup_1_failure_in_teardown(self, pytester: Pytester) -> None:
1569+
testpath = pytester.makepyfile(
1570+
"""
1571+
import unittest
1572+
class MyTestCase(unittest.TestCase):
1573+
@classmethod
1574+
def setUpClass(cls):
1575+
def cleanup(n):
1576+
raise Exception(f"fail {n}")
1577+
cls.addClassCleanup(cleanup, 1)
1578+
def test(self):
1579+
pass
1580+
"""
1581+
)
1582+
result = pytester.runpytest("-s", testpath)
1583+
result.assert_outcomes(passed=1, errors=1)
1584+
result.stdout.fnmatch_lines(
1585+
[
1586+
"*ERROR at teardown of MyTestCase.test*",
1587+
"*Exception: fail 1",
1588+
]
1589+
)
1590+
1591+
15031592
def test_traceback_pruning(pytester: Pytester) -> None:
15041593
"""Regression test for #9610 - doesn't crash during traceback pruning."""
15051594
pytester.makepyfile(

0 commit comments

Comments
 (0)