]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-120665: make unittest loaders avoid loading test cases that are abstract base...
authorblhsing <blhsing@gmail.com>
Wed, 17 Jun 2026 11:48:09 +0000 (04:48 -0700)
committerGitHub <noreply@github.com>
Wed, 17 Jun 2026 11:48:09 +0000 (07:48 -0400)
Lib/test/test_unittest/test_loader.py
Lib/unittest/loader.py
Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst [new file with mode: 0644]

index cdff6d1a20c8dfda384178ab48403074e66409bd..f4e50a3dccb2bc22439f8d809f1c26945607525c 100644 (file)
@@ -1,3 +1,4 @@
+import abc
 import functools
 import sys
 import types
@@ -98,6 +99,22 @@ class Test_TestLoader(unittest.TestCase):
         self.assertIsInstance(suite, loader.suiteClass)
         self.assertEqual(list(suite), [])
 
+    # "Do not load any tests from a TestCase-derived class that is an abstract
+    # base class."
+    def test_loadTestsFromTestCase__from_abc_TestCase(self):
+        class FooBase(unittest.TestCase, metaclass=abc.ABCMeta):
+            @abc.abstractmethod
+            def test(self): ...
+        class Foo(FooBase):
+            def test(self): pass
+
+        empty_suite = unittest.TestSuite()
+
+        loader = unittest.TestLoader()
+        suite = loader.loadTestsFromTestCase(Foo)
+        self.assertEqual(loader.loadTestsFromTestCase(FooBase), empty_suite)
+        self.assertEqual(list(suite), [Foo('test')])
+
     ################################################################
     ### /Tests for TestLoader.loadTestsFromTestCase
 
@@ -252,6 +269,24 @@ class Test_TestLoader(unittest.TestCase):
 
         self.assertRaisesRegex(TypeError, "some failure", test.m)
 
+    # Check that loadTestsFromModule skips abstract base classes derived from
+    # TestCase, which can't be instantiated.
+    def test_loadTestsFromModule__skip_abc_TestCase(self):
+        m = types.ModuleType('m')
+        class MyTestCaseBase(unittest.TestCase, metaclass=abc.ABCMeta):
+            @abc.abstractmethod
+            def test(self):
+                ...
+        class MyTestCase(MyTestCaseBase):
+            def test(self):
+                pass
+        m.testcase_1 = MyTestCaseBase
+        m.testcase_2 = MyTestCase
+        loader = unittest.TestLoader()
+        suite = loader.loadTestsFromModule(m)
+        expected = [loader.suiteClass([MyTestCase('test')])]
+        self.assertEqual(list(suite), expected)
+
     ################################################################
     ### /Tests for TestLoader.loadTestsFromModule()
 
@@ -1052,6 +1087,22 @@ class Test_TestLoader(unittest.TestCase):
             if module_name in sys.modules:
                 del sys.modules[module_name]
 
+    # "The specifier should not refer to a test method in a TestCase-derived
+    # subclass that is an abstract base class"
+    def test_loadTestsFromNames__testmethod_in_abc_TestCase(self):
+        m = types.ModuleType('m')
+        class Foo(unittest.TestCase, metaclass=abc.ABCMeta):
+            @abc.abstractmethod
+            def test_1(self): ...
+            def test_2(self): pass
+        m.Foo = Foo
+
+        loader = unittest.TestLoader()
+        for name in 'Foo.test_1', 'Foo.test_2':
+            with self.subTest(name=name), self.assertRaisesRegex(TypeError,
+                    "Cannot instantiate abstract test case Foo"):
+                loader.loadTestsFromNames([name], m)
+
     ################################################################
     ### /Tests for TestLoader.loadTestsFromNames()
 
index a52950dad224ee7dbf44ae7c6cdc1d4d17a2fb94..697520246f0e3c61c043fb0e6f74716ad0e1af73 100644 (file)
@@ -1,5 +1,6 @@
 """Loading unittests."""
 
+import inspect
 import os
 import re
 import sys
@@ -84,8 +85,10 @@ class TestLoader(object):
             raise TypeError("Test cases should not be derived from "
                             "TestSuite. Maybe you meant to derive from "
                             "TestCase?")
-        if testCaseClass in (case.TestCase, case.FunctionTestCase):
-            # We don't load any tests from base types that should not be loaded.
+        if (testCaseClass in (case.TestCase, case.FunctionTestCase) or
+            inspect.isabstract(testCaseClass)):
+            # We don't load any tests from base types that should not be loaded,
+            # and abstract base classes that can't be instantiated
             testCaseNames = []
         else:
             testCaseNames = self.getTestCaseNames(testCaseClass)
@@ -103,6 +106,7 @@ class TestLoader(object):
                 isinstance(obj, type)
                 and issubclass(obj, case.TestCase)
                 and obj not in (case.TestCase, case.FunctionTestCase)
+                and not inspect.isabstract(obj)
             ):
                 tests.append(self.loadTestsFromTestCase(obj))
 
@@ -181,6 +185,9 @@ class TestLoader(object):
         elif (isinstance(obj, types.FunctionType) and
               isinstance(parent, type) and
               issubclass(parent, case.TestCase)):
+            if inspect.isabstract(parent):
+                raise TypeError(
+                    "Cannot instantiate abstract test case %s" % parent.__name__)
             name = parts[-1]
             inst = parent(name)
             # static methods follow a different path
diff --git a/Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst b/Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst
new file mode 100644 (file)
index 0000000..27e9398
--- /dev/null
@@ -0,0 +1 @@
+Fixed an issue where ``unittest`` loaders would load and instantiate :class:`unittest.TestCase`-derived subclasses that are also abstract base classes, which can't be instantiated.