From f570152afa9bf3ac0da6efc5642175c5506db04a Mon Sep 17 00:00:00 2001 From: zhangli091011 Date: Mon, 20 Jul 2026 10:48:08 +0800 Subject: [PATCH 1/2] Treat malformed unittest skips as collection errors Preserve deliberate unittest.SkipTest module skips while reporting SkipTest raised by unittest's generated skip decorator wrapper as an import-time collection error. Co-authored-by: OpenCode --- AUTHORS | 1 + changelog/10821.bugfix.rst | 1 + src/_pytest/runner.py | 13 ++++++++++++- testing/test_unittest.py | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 changelog/10821.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 33ee644ea68..6c184a5b76c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -529,6 +529,7 @@ Zac Palmer Laporte Zach Snicker Zachary Kneupper Zachary OBrien +zhangli091011 Zhouxin Qiu Zoltán Máté Zsolt Cserna diff --git a/changelog/10821.bugfix.rst b/changelog/10821.bugfix.rst new file mode 100644 index 00000000000..55e276475ff --- /dev/null +++ b/changelog/10821.bugfix.rst @@ -0,0 +1 @@ +Malformed uses of :func:`unittest.skip` during test module import now cause a collection error instead of silently skipping the module. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 3f03cfaff77..31736338cf2 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -415,7 +415,18 @@ def collect() -> list[Item | Collector]: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") if unittest is not None: - skip_exceptions.append(unittest.SkipTest) + exception = call.excinfo.value + if isinstance(exception, unittest.SkipTest): + traceback = exception.__traceback__ + assert traceback is not None + while traceback.tb_next is not None: + traceback = traceback.tb_next + frame = traceback.tb_frame + if not ( + frame.f_code.co_name == "skip_wrapper" + and frame.f_globals.get("__name__") == "unittest.case" + ): + skip_exceptions.append(unittest.SkipTest) if isinstance(call.excinfo.value, tuple(skip_exceptions)): outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 20287d12cb3..2fb34c8d804 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1715,6 +1715,27 @@ def test_it2(self): pass assert reprec.ret == ExitCode.NO_TESTS_COLLECTED +def test_unittest_skip_decorator_misuse_is_collection_error( + pytester: Pytester, +) -> None: + pytester.makepyfile( + """ + import unittest + + broken = unittest.skip("broken") + + class TestIt(unittest.TestCase): + @broken("not a test") + def test_it(self): pass + """ + ) + + result = pytester.runpytest() + + result.assert_outcomes(errors=1, skipped=0) + result.stdout.fnmatch_lines(["*unittest.case.SkipTest: broken*"]) + + def test_abstract_testcase_is_not_collected(pytester: Pytester) -> None: """Regression test for #12275.""" pytester.makepyfile( From 3851be6c7d13431b479d5e60b5501b886675ee3d Mon Sep 17 00:00:00 2001 From: zhangli091011 Date: Mon, 20 Jul 2026 11:11:10 +0800 Subject: [PATCH 2/2] Allow Twisted's SkipTest name in regression output Twisted replaces unittest.case.SkipTest during its integration tests, while preserving the collection-error behavior under test. Co-authored-by: OpenCode --- testing/test_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2fb34c8d804..c0da9e1cf16 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1733,7 +1733,7 @@ def test_it(self): pass result = pytester.runpytest() result.assert_outcomes(errors=1, skipped=0) - result.stdout.fnmatch_lines(["*unittest.case.SkipTest: broken*"]) + result.stdout.fnmatch_lines(["*SkipTest: broken*"]) def test_abstract_testcase_is_not_collected(pytester: Pytester) -> None: