MongoDB 仓库内嵌的 testtools 2.7.1:写给测试框架作者的进阶指南(for framework folk)

MongoDB 仓库内嵌的 testtools 2.7.1:写给测试框架作者的进阶指南(for framework folk) MongoDB 仓库内嵌的 testtools 2.7.1写给测试框架作者的进阶指南for framework folk【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo在 WiredTiger 的 Python 测试基础设施中MongoDB 仓库以第三方依赖的形式内嵌了一份完整的 testtools 2.7.1 发行版位于 src/third_party/wiredtiger/test/3rdparty/testtools-2.7.1。其中 for-framework-folk.rst 专门面向测试框架作者编写测试运行器、维护大型单测工程、让两套测试框架互操作、或把测试套件并行化到集群上的开发者。本文以该文档为骨架结合内嵌的 testtools 源码逐项展开从 TestCase 级扩展异常处理、RunTest 定制、测试改名、延迟失败到 TestResult/StreamResult 全家桶再到 TestSuite/TestRunner 层的并发与过滤机制帮助读者理解这套给框架作者的乐高积木到底怎么拼、以及每个部件在源码中的落点。testtoolsunittest 的扩展层及其在本仓库中的位置testtools 的定位是对标准 Python unittest 库的扩展见 testtools/__init__.py 开头的模块 docstring。for-framework-folk.rst 开篇即点明目标读者测试运行器test runner的作者维护大型单元测试工程的团队需要让一个测试框架与另一个框架协同工作的人希望把测试套件并行运行在异构机器集群上的开发者。该文档同时声明自己是一份摘要细节需查阅 testtools API 文档。原文档提到的另一篇 for-test-authors.rst 面向普通测试作者而本篇关注的是框架侧能力。在本仓库中这份 testtools 被 WiredTiger 测试套件使用其上层还配套了 concurrencytest、python-subunit 等同样依赖 testtools 的组件——subunit 系列工具大量调用 testtools 的 TestResult/StreamResult API这正是原文档所说让框架之间协同的典型场景。Extensions to TestCaseTestCase 与 TestSuite 的复合模式扩展原文档指出testtools 不仅提供 TestCase 专有方法还提供了同样适用于 TestCase 的 TestSuite 扩展方法原因在于TestCase与TestSuite遵循 Composite组合模式——测试套件和单个测试对外呈现统一的run(result)接口因此任何作用于套件的能力天然也能作用于其中的单个用例。这一点在源码中直接体现TestCase与ConcurrentTestSuite都实现了run、sorted_tests、filter_by_ids等方法。自定义异常处理exception_handlerstesttools 允许控制测试异常的处理方式向testtools.TestCase的self.exception_handlers列表中插入一个(异常类, handler)对即可。原文档的示例 self.exception_handlers.insert(-1, (ExceptionClass, handler))此后只要setUp、tearDown或测试方法本身抛出了ExceptionClasshandler就会以测试用例、测试结果对象、异常对象三个参数被调用。原文档建议的使用场景是当你想引入一种新的测试结果类型——即认为addError、addFailure等标准结局不够用时。源码印证了这一机制的两个关键点默认处理器链。testcase.py 中TestCase.__init__初始化了一条默认的exception_handlers从具体到宽泛依次为skipException - _report_skip、failureException - _report_failure、_ExpectedFailure、_UnexpectedSuccess最后兜底Exception - _report_error。匹配与执行。RunTest在捕获用户代码异常后按列表顺序找到第一个isinstance匹配的处理器并调用若没有任何处理器匹配则走last_resort并把异常重新抛出。这一逻辑在 runtest.py 的_run_prepared_result中可以看到if self._exceptions: e self._exceptions.pop() for exc_class, handler in self.handlers: if isinstance(e, exc_class): handler(self.case, self.result, e) break else: self.last_resort(self.case, self.result, e) raise e因此用insert(-1, ...)插入自定义处理器就是把它排在兜底的Exception之前——这正是原文档示例中下标取-1的原因。注意exception_handlers与另一个内部列表__exception_handlers由addOnException维护见 testcase.py职责不同后者只在异常发生时收集诊断信息、不能改变最终报告为 success/failure/error 的走向。控制测试执行自定义 RunTest如果想控制的不仅是异常如何被处理而是测试如何被执行可以给TestCase提供一个自定义RunTest。RunTest对象可以改变测试执行的方方面面。要适配testtools.TestCase一个RunTest必须满足有一个工厂接受一个 test 参数、一个可选的异常处理器列表、以及一个可选的last_resort处理器工厂返回的实例必须有一个run()方法接受一个可选的TestResult对象。默认实现是testtools.runtest.RunTest它按照 Python 标准库 unittest 的原味方式依次调用setUp、测试方法、tearDown和 cleanups。指定方式有三种优先级从高到低# 方式一类级作用于整个 TestCase 类的所有测试 class SomeTests(TestCase): run_tests_with CustomRunTestFactory # 方式二方法级装饰器 class SomeTests(TestCase): run_test_with(CustomRunTestFactory, extra_arg42, foowhatever) def test_something(self): pass # 方式三构造时传入 runTest 参数覆盖以上两者 MyTest(test_something, runTestCustomRunTestFactory)源码中三者的衔接逻辑在 testcase.py__init__先弹出runTest关键字参数若为空则回退到测试方法上的_run_test_with属性由run_test_with装饰器写入再回退到类属性run_tests_with默认为RunTest。而 run_test_with 的实现是在被装饰函数上挂一个_run_test_with工厂闭包并保留了向后兼容路径当旧式工厂不支持last_resort参数时会捕获TypeError重试。此外装饰器文档字符串特别提醒若与其他装饰器叠加使用run_test_with应放在最外层或确保外层装饰器透传被包装函数的属性functools.wraps等可以帮忙。测试改名clone_test_with_new_idtesttools.clone_test_with_new_id用于把已构造的测试用例实例复制为一个新名字的实例是实现参数化测试test parameterization的基础工具。其实现非常直白testcase.pydef clone_test_with_new_id(test, new_id): return _clone_test_id_callback(test, lambda: new_id)内部对源测试做copy.copy并把id方法替换为返回new_id的回调。注意其文档限定只适用于已构造但未执行的测试。相关地源码中还有WithAttributes混入类和attr装饰器testcase.py它们通过在测试 id 末尾追加[foo,bar]形式属性段的方式标记用例是框架作者做特性维度追踪的常用手段。延迟失败force_failure把testtools.TestCase.force_failure实例变量置为True会使testtools.RunTest在测试执行完之后把该测试判为失败。原文档指出的用途你希望测试最终失败但不想阻止剩余测试代码执行例如故意触发一条慢速错误路径但要先让测试把现场数据写出来。执行时序可以从 runtest.py 的_run_core中确认setUp - 测试方法 - tearDown - cleanups全部走完之后才检查force_failure并调用_raise_force_fail_error只有在这一切都没有失败时才报告addSuccess。因此延迟是精确语义——测试主体的所有副作用都会先发生。配套测试用例 test_testcase.py 的test__force_failure_fails_test验证了这一行为。异常格式化testtools_tb_localstesttools 的TestCase会自行格式化异常。实例属性__testtools_tb_locals__控制格式化后的异常里是否包含局部变量。从源码看这个值并不是让用户随意设置的而是由结果对象决定runtest.py 在执行前执行self.case.__testtools_tb_locals__ getattr(result, tb_locals, False)即若当前TestResult声明了tb_locals True则 traceback 会附带局部变量快照——这对调试远程/并发测试时收集现场信息很有价值。Test placeholders把非测试事件记入测试结果原文档指出有时需要往测试套件里添加一些并非真正测试的条目例如把测试发现discovery阶段的导入失败表示为测试这样结果对象就不用为它们做特殊处理。testtools 提供两个占位符对象PlaceHolder接受一个 test id 和可选描述运行时报告成功ErrorHolder接受一个 test id、一个 error 和可选的简短描述运行时报告该 error。原文档强调占位符最适合记录发生在测试套件之外、但与结果高度相关的事件。示例 suite TestSuite() suite.add(PlaceHolder(I record an event)) suite.run(TextTestResult(verboseTrue)) I record an event [OK]实现上testcase.py 中PlaceHolder支持outcome/error/details参数默认addSuccess指定 error 时走addError且其details会在构造时通过gather_details合并进自身、在id()上附加描述文本ErrorHolder则是直接以outcomeaddError构造PlaceHolder的便捷工厂。由于占位符与真实测试共享同一run(result)接口任何 TestResult 都能统一消费它们——这就是原文档所说结果对象不需要特殊处理的机制来源。Test instance decoratorsDecorateTestCaseResultDecorateTestCaseResult在run/__call__被调用时回调你的代码允许替换实际用来运行测试的 result 对象。原文档给出的场景当你在一个不知道你的测试用例需要什么样的 test runner 环境中运行时它可以帮助向StreamResult迁移。示例 suite TestSuite() suite DecorateTestCaseResult(suite, ExtendedToOriginalDecorator)对应实现位于 testcase.py其构造签名接收source待包装的测试或套件与decorator一个可调用对象接收 result 返回替换后的 result。从源码结构看它只拦截run/__call__两个入口对测试本体零侵入因此适合作为外部宿主框架与测试用例之间的适配层。Extensions to TestResultStreamResult 体系StreamResult 的设计原则StreamResult是处理测试进度的新式 API支持并发与分布式测试避免了旧TestResult的诸如 multiplexer 中缓冲等问题。原文档列出了三条关键设计原则不要求预先知道全部测试Nothing that requires up-front knowledge of all tests应对并发执行环境测试可能分布在多个进程甚至多台机器上因此必须允许多个测试同时处于活动状态、显式供应时间、能区分运行在不同上下文中的测试、并移除测试必然在同一进程中的假设API 尽可能简单——每个部件只做好一件事。原文档还分析了被替代的TestResultAPI 实际上混合了三类客户端角色执行中的TestCase向结果对象报告活动测试运行器用 API 判断本次运行是否有错误、跑了多少测试TestCase反过来查询结果对象是否应当中止运行。StreamResult体系的处理方式是把这三者拆成三个独立 API再混合出兼容适配器以支持渐进式迁移。StreamSummary第二块 API 是StreamSummary负责归集错误、检测未完成的测试、统计测试数量并提供与TestResult对应方面的兼容接口。源码实现见 real.py它继承StreamResult把事件流折叠成wasSuccessful、errors、testCounts等汇总视图——即对应原文档所说compatible API with those aspects of TestResult。TestControl第三块 API 是TestControl提供旧TestResult中shouldStop/stop的职责。TestControl可以与StreamFailFast搭配在观察到失败时触发中止整次测试运行。原文档特别提示在分布式环境中中止多个 worker需要把分布式环境自身的信号机制接到每个 worker 进程内的TestControl上——这是一个把进程间通信留给宿主框架的明确边界。配套迁移适配器ExtendedToStreamDecorator混合对象把Extended与Stream两套 TestResult API 合并进一个类但只向外发射StreamResult事件。适用场景你希望得到一条 Stream 事件流但不能确定将要运行的测试都已更新到 StreamResult API。StreamToExtendedDecorator方向相反的简单转换器把StreamResult事件翻译为ExtendedTestResultAPI 输出。适用场景测试用例按新 API 输出事件但宿主TestResult不支持status/file等新方法。StreamTagger事件标签过滤器StreamTagger是一个StreamResult过滤器用于从事件上添加或移除标签。原文档示例 from testtools import StreamTagger sink StreamResult() result StreamTagger([sink], set([add]), set([discard])) result.startTestRun() # Run tests against result here. result.stopTestRun()即把add列表中的标签打上、把discard列表中的标签剥掉再转发给 sink。实现位于 real.py继承自CopyStreamResult多路复制基类在转发每个status事件前对tags字段做集合运算。StreamToDict面向分析器的简化出口StreamToDict是处理 Stream 流的简化 API每个测试的事件被缓冲直至该测试完成然后作为一个平凡字典整体报告给回调。原文档评价这是写分析器analyser的最省心事——可以完全忽略事件管道细节直接操作结果。示例 from testtools import StreamToDict def handle_test(test_dict): ... print(test_dict[id]) result StreamToDict(handle_test) result.startTestRun() # Run tests against result here. # At stopTestRun() any incomplete buffered tests are announced. result.stopTestRun()注意最后一行注释的语义stopTestRun()时会把仍被缓冲的未完整测试宣告出来这与StreamSummary中检测未完成测试的职责呼应保证分析器不会静默丢失数据。StreamToQueue多线程结果上报StreamToQueue是一个StreamResult装饰器用于多线程同时上报测试每个方法把事件作为简单字典提交到给定的Queue对象中。原文档建议配合ConcurrentStreamTestSuite使用后文介绍。TimestampingStreamResult补时间戳该装饰器为缺少时间戳的事件补打时间戳。原文档的解释是允许你写出最简单的事件生成器再让事件经此装饰器得到带时间戳的数据只要时间戳器看到事件之前没有缓冲/排队或阻塞打上的时间戳就和原始事件自带时间戳一样精确。对并发/分布式场景尤其关键——事件产生时刻与消费时刻可能差很多越早打上时间戳越准。StreamResultRouter动态路由StreamResultRouter是一个把事件转发给任意一组目标StreamResult的路由器。没有匹配规则的事件交给 fallback 结果对象处理路由映射可以在运行时更改从而获得很高的灵活性与响应性。原文档同时指出正因为映射是动态的、且两个规则可能指向同一个接收方startTestRun/stopTestRun的处理粒度细化了、交由用户负责若没有提供 fallback不可路由的事件会抛出异常。原文档示例 router StreamResultRouter() sink doubles.StreamResult() router.add_rule(sink, route_code_prefix, route_prefix0, ... consume_routeTrue) router.status(test_idfoo, route_code0/1, test_statusuxsuccess)该调用会把route_code中的0/前缀去掉consume_routeTrue然后像这样转发 sink.status(test_idfoo, route_code1, test_statusuxsuccess)原文档建议用pydoc testtools.StreamResultRouter查看完整参数。实现见 real.py其add_rule接受目标结果 匹配谓词 关键字匹配参数的三元组。addSkip、time 与 TestResult 基础增强TestResult.addSkip测试被跳过时在结果对象上被调用。testtools.TestResult把跳过原因记录在skip_reasons实例字典中后续可以像报告成功测试一样报告它们。TestResult.time控制TestResult使用的时间源使得在不同机器、不同线程中采集的测试结果也能获得准确的计时。startTestRun/stopTestRunPython 2.7 引入的全局运行钩子在整次测试运行前后被调用stopTestRun对需要输出汇总信息的结果对象特别有用。testtools.TestResult提供这两个方法的默认实现且默认运行器会在合适的时机调用它们startTestRun会清空结果对象上的错误、失败等状态使其看起来像还没有跑过任何测试。ThreadsafeForwardingResult / MultiTestResult / TestResultDecoratorThreadsafeForwardingResult把活动转发给另一个结果对象但通过信号量同步保证单个测试的全部活动成批到达。这样就不期望远测试并发上报的简单 TestResult 也能安全消费多个测试线程或进程的活动。原文档特别提醒若为同一测试提供了多个 error目标会把每个 error 视为一个独立的完整测试。实现见 real.py。MultiTestResult把事件分发给多个测试结果。用它把多个不同的结果对象合并成一个可传给TestCase.run()的对象a TestResult() b TestResult() combined MultiTestResult(a, b) combined.startTestRun() # Calls a.startTestRun() and b.startTestRun()其每个方法都会返回一个由各组件返回值组成的元组。TestResultDecorator严格说不是TestResult而是实现了 testtools 扩展 TestResult 接口的装饰基类可以通过继承来创建包装 TestResult 的对象。TextTestResult 与 ExtendedToOriginalDecoratorTextTestResult提供与标准库 UI 非常接近的文本界面关键差异是支持扩展结局与 details API且完全封装在结果对象内部——无需 TestRunner 对象即可使用它还是安静结果没有点号输出或 verbose 模式原文档说明这些限制将尽快修正。ExtendedToOriginalDecorator把旧式TestResult如老版本 Python 中的对象适配成满足 testtools TestResult API 的形式。它也是RunTest._run_prepared_result内部使用的默认装饰器确保新 API 可以放心地被客户端代码调用见 runtest.py 的注释。Test Doublestestresult.doublestesttools.testresult.doubles中提供三个 testtools 自测使用的测试替身Python26TestResult、Python27TestResult、ExtendedTestResult。每个只实现一种 TestResult API 变体并把活动日志记录到self._events列表中。它们对编写自己扩展的人开放使用。实现位于 doubles.py其中还有一个记录 Stream 事件的StreamResult替身doubles.py——原文档示例中doubles.StreamResult()指的就是它是单测里断言事件流内容的标准做法。Extensions to TestSuite并发、夹具与排序过滤ConcurrentTestSuite面向并行测试的 TestSuite它配合一个以某种并行方式运行单个套件的 helper例如 fork、交给子进程、交给计算云、或简单地用线程。它用 helper 得到一组独立的、带run(result)方法的可运行对象然后用线程把它们全部跑起来并用ThreadsafeForwardingResult把各个线程的活动归并。实现见 testsuite.py。ConcurrentStreamTestSuiteConcurrentTestSuite的 Stream 版使用 StreamResult API 而非 TestResult API 来协调若干测试/套件的并发运行每个测试/套件配一个StreamToQueue。具体地每个测试/套件会得到一个由ExtendedToStreamDecoratorTimestampingStreamResult包装的StreamToQueue实例最终转发到调用ConcurrentStreamTestSuite.run时传入的StreamResult。原文档强调它是一层thin shim薄垫片如需要实现自己特化的并发套件形式非常容易。实现见 testsuite.py。从源码结构看这个组合恰好串起了前文介绍的四类 Stream 部件ExtendedToStreamDecorator兼容旧测试、TimestampingStreamResult尽早打时间戳、StreamToQueue线程安全入队、以及最终汇聚的StreamResult消费端——是一个很好的部件如何拼装成系统的范例。FixtureSuite在运行任何测试之前先建立某个 fixture、在所有测试跑完之后拆掉它的套件。原文档明确说明一个设计约束该 fixture不会暴露给任何测试使用——因为套件向其包含的测试传递信息没有标准通道且还没收集到足够数据来判断这样一个通道应该长什么样甚至还没决定它是好主意。实现见 testsuite.py。sorted_tests一致的测试顺序由于 TestSuite/TestCase 的组合结构排序测试是有难度的——你无法知道自定义 Suite 实现里嵌入了什么行为。为了在使用测试发现时得到一致的测试顺序testtools 的做法是对标准TestSuite进行展平并排序同时定义一个新方法sorted_tests供非标准 TestSuite 知道何时应该对自己的测试排序。原文档指出示例实现可见FixtureSuite.sorted_tests并且当套件中存在重复 test id 时抛出ValueError。filter_by_ids按 id 选择测试子集与排序同理运行测试子集同样困难——标准 run 接口没有提供限制运行范围的途径。原文档强调设计取舍不要把选择与执行这两个问题搅在一起而是定义一个按唯一 test id 过滤套件或用例中测试的方法。原文档建议如果你在编写自定义的包装套件可以考虑实现filter_by_ids以支持该能力多数继承unittest.TestSuite的包装器按 testtools.testsuite.filter_by_ids 的实现细节可以直接正常工作。Extensions to TestRunner自定义测试列举为支持测试的自定义列举testtools.run.TestProgram会尝试对TestRunner调用list如果运行器没有该方法则回退到一个通用实现。这使得框架作者可以接管列出将运行哪些测试的行为例如从远端主机拉取清单而无需改动运行逻辑本身。小结把 testtools 当框架作者工具箱使用通读 for-framework-folk.rst 并结合内嵌源码可以总结出 testtools 面向框架作者的三层能力执行层TestCase/RunTestexception_handlers定制结局、run_tests_with/run_test_with/构造参数三级 RunTest 覆盖、clone_test_with_new_id参数化、force_failure延迟失败、__testtools_tb_locals__现场收集——全部围绕让测试按我的规则跑、按我的格式报。结果层TestResult/StreamStreamResult StreamSummary TestControl三分法解决并发/分布式报告StreamToDict/StreamToQueue/StreamTagger/TimestampingStreamResult/StreamResultRouter提供缓冲、入队、打标签、补时间戳、动态路由五种管道元件MultiTestResult/ThreadsafeForwardingResult/各 Decorator 负责新旧 API 与多目标的适配。套件与运行器层TestSuite/TestRunnerConcurrentTestSuite/ConcurrentStreamTestSuite并行、FixtureSuite夹具、sorted_tests/filter_by_ids解决组合结构下的顺序与选择难题、TestProgram的list钩子接管测试列举。需要说明的适用边界这是 MongoDB 仓库内 WiredTiger 测试树中内嵌的testtools 2.7.1快照文档面向 Python 2.6–3.x 时代的双栈 API因此保留了Python26TestResult/Python27TestResult等兼容件文中所有行为描述以该版本源码为准若使用 pip 安装的其他版本 testtools个别 API 细节可能不同。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考