)
cpython 修复 unittest 加载器实例化抽象基类 TestCase 子类的问题gh-120665【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读在 CPython 的unittest框架中TestLoader负责从模块、类名等入口发现并加载测试。本次修复对应 Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst解决了这样一个问题当用户定义了一个**同时继承unittest.TestCase且带有抽象方法的抽象基类ABC**时unittest的加载器loader会把它当成普通测试类去实例化从而抛出TypeError无法实例化抽象类导致测试发现流程失败。阅读本文后你将掌握抽象测试基类在unittest三条加载路径下的正确处理方式、inspect.isabstract的判定原理以及如何用测试用例验证这一行为。问题背景为什么不能实例化抽象 TestCasePython 的abc模块提供了抽象基类机制。当一个类带有未实现的抽象方法即__abstractmethods__非空时Python 会禁止直接实例化它调用构造器会抛出TypeError: Cant instantiate abstract class ...。在测试工程中这存在一个合理的用法定义一个抽象测试基类把公共的测试步骤或测试数据抽到基类中仅预留抽象方法让各具体子类去实现。例如import abc import unittest class BaseAPITest(unittest.TestCase, metaclassabc.ABCMeta): abc.abstractmethod def setUp(self): # 强制子类实现 ... def test_status_code_is_200(self): # 子类 setUp 已准备好 self.response self.assertEqual(self.response.status_code, 200) class TestLoginAPI(BaseAPITest): def setUp(self): self.response do_login()问题在于unittest的TestLoader在设计上只要看到TestCase的子类就尝试实例化并收集其test*方法。抽象基类BaseAPITest本身无法实例化loader 却仍会去调用它于是测试发现test discovery过程直接报错中断而不是优雅地跳过这个抽象基类、只加载其具体子类。修复内容三条加载路径统一跳过抽象基类本次修复的核心位于 Lib/unittest/loader.py 的TestLoader类。它针对unittest的三个加载入口分别做了处理加载路径方法修复策略按测试类加载loadTestsFromTestCase检测到抽象基类时返回空测试套件不收集任何测试按模块加载loadTestsFromModule遍历模块属性时跳过抽象基类不进入后续加载逻辑按名字加载点分名称loadTestsFromName/loadTestsFromNames若目标父类为抽象基类直接抛出明确的TypeError1. loadTestsFromTestCase抽象基类不再收集测试loadTestsFromTestCase是加载器的核心方法负责把给定的测试类实例化为一个个测试用例# Lib/unittest/loader.py if (testCaseClass in (case.TestCase, case.FunctionTestCase) or inspect.isabstract(testCaseClass)): # We dont load any tests from base types that should not be loaded, # and abstract base classes that cant be instantiated testCaseNames [] else: testCaseNames self.getTestCaseNames(testCaseClass) if not testCaseNames and hasattr(testCaseClass, runTest): testCaseNames [runTest] loaded_suite self.suiteClass(map(testCaseClass, testCaseNames))关键点在于条件分支一旦inspect.isabstract(testCaseClass)为真testCaseNames被置为空列表后续map(testCaseClass, testCaseNames)就不会对抽象类执行任何构造调用最终返回一个空套件。这样既不会触发TypeError也保留了基类不是测试目标的正确语义——只有它的具体子类才应该被加载。2. loadTestsFromModule模块扫描时直接过滤loadTestsFromModule会遍历模块的dir(module)逐个检查属性是否为TestCase子类。修复后过滤条件同时要求该对象不是抽象基类# Lib/unittest/loader.py if ( 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))这是python -m unittest discover这类按目录递归扫描场景的关键防线模块中同时存在抽象基类和它的具体子类时只有具体子类会被收集进测试套件。3. loadTestsFromName按名字指定时给出明确报错当用户通过点分名称如pkg.mod.Foo.test_1直接指定某个测试方法而Foo恰好是抽象基类时加载器无法静默跳过——因为用户明确点名了目标。此时修复会抛出信息明确的TypeError# Lib/unittest/loader.py 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)错误消息Cannot instantiate abstract test case 类名直接告诉用户这个类是抽象基类无法作为具体测试目标实例化。支撑实现inspect.isabstract 如何判定抽象类上述三处修复都依赖标准库 Lib/inspect.py 中的inspect.isabstract(object)。从源码看它的判定分三层# Lib/inspect.py def isabstract(object): Return true if the object is an abstract base class (ABC). if not isinstance(object, type): return False if object.__flags__ TPFLAGS_IS_ABSTRACT: return True if not issubclass(type(object), abc.ABCMeta): return False if hasattr(object, __abstractmethods__): # It looks like ABCMeta.__new__ has finished running; ...首先排除非类对象然后检查类型标志TPFLAGS_IS_ABSTRACT该标志由 CPython 的类型对象机制维护见 Include/cpython/object.h 中相关的tp_flags定义对普通ABCMeta元类创建的类进一步检查__abstractmethods__属性是否存在且非空——这是abc模块在ABCMeta.__new__完成后写入的抽象方法清单。这也是为什么修复能覆盖元类为abc.ABCMeta的TestCase子类以及继承自既有抽象测试基类的中间基类只要类上仍有未实现的抽象方法isabstract就返回True加载器即将其排除。回归测试三条路径的行为验证本修复配套的测试位于 Lib/test/test_unittest/test_loader.py覆盖了全部三条加载路径按类加载抽象基类返回空套件test_loadTestsFromTestCase__from_abc_TestCase定义了抽象基类FooBase带abc.abstractmethod及其具体子类Foo# Lib/test/test_unittest/test_loader.py class FooBase(unittest.TestCase, metaclassabc.ABCMeta): abc.abstractmethod def test(self): ... class Foo(FooBase): def test(self): pass loader unittest.TestLoader() suite loader.loadTestsFromTestCase(Foo) self.assertEqual(loader.loadTestsFromTestCase(FooBase), empty_suite) self.assertEqual(list(suite), [Foo(test)])断言明确FooBase的加载结果是空套件而具体子类Foo正常产出Foo(test)测试用例。按模块加载模块扫描跳过抽象基类test_loadTestsFromModule__skip_abc_TestCase模拟一个模块同时暴露MyTestCaseBase抽象与MyTestCase具体# Lib/test/test_unittest/test_loader.py m.testcase_1 MyTestCaseBase m.testcase_2 MyTestCase suite loader.loadTestsFromModule(m) expected [loader.suiteClass([MyTestCase(test)])] self.assertEqual(list(suite), expected)只有MyTestCase进入结果套件抽象基类被静默跳过。按名字加载抛出明确 TypeErrortest_loadTestsFromNames__testmethod_in_abc_TestCase验证对抽象类中的任意方法名包括具体方法test_2显式指定时都会报错# Lib/test/test_unittest/test_loader.py for name in Foo.test_1, Foo.test_2: with self.subTest(namename), self.assertRaisesRegex(TypeError, Cannot instantiate abstract test case Foo): loader.loadTestsFromNames([name], m)注意test_2是普通方法而非抽象方法但父类Foo是抽象基类因此同样拒绝实例化——错误信息的正则匹配也固定了Cannot instantiate abstract test case Foo这条用户可见文案。对测试工程的实际影响与使用建议这次修复对依赖抽象测试基类的项目是行为修正而非破坏性变更修复前python -m unittest discover或loadTestsFromModule遇到抽象TestCase子类会尝试实例化并抛TypeError测试发现流程整体失败基类与具体子类同模块时尤为致命修复后抽象基类在所有自动发现路径下被静默跳过只有被用户显式点名loadTestsFromName时才抛出明确的TypeError错误信息足以定位问题。实践上可以放心采用抽象测试基类 具体子类的组织方式例如把公共断言、setUp/tearDown骨架、数据准备逻辑放入抽象基类用abc.abstractmethod强制子类补齐差异部分。需要留意的是unittest的自动发现只认test*前缀方法抽象基类被跳过并不影响其具体子类继承到的公共test*方法正常执行——正如测试中Foo(test)所示具体子类依旧会运行从基类继承的全部测试方法。小结本次修复gh-120665针对unittest加载器与抽象基类的兼容性缺口在loadTestsFromTestCase、loadTestsFromModule、loadTestsFromName三条路径上统一引入inspect.isabstract检查自动发现场景静默跳过抽象测试基类显式点名场景抛出带类名的明确TypeError。实现集中在 Lib/unittest/loader.py配套回归测试位于 Lib/test/test_unittest/test_loader.py为抽象测试基类这一惯用模式提供了稳定、可预期的行为保障。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考