Python doctest模块详解:让文档成为可执行的自动化测试
1. 项目概述
如果你写过Python代码,尤其是写过一些库或者工具函数,大概率会在函数的docstring(文档字符串)里写几个使用示例。比如,你写了一个计算阶乘的函数,可能会顺手在文档里写上>>> factorial(5)和预期的120。这些示例代码,对阅读文档的人来说是极好的指引,但对你来说,它们仅仅是“注释”吗?你有没有想过,这些写在注释里的代码,其实可以直接运行,并且验证其结果是否正确?这就是doctest模块的核心思想:让文档成为可执行的测试。
doctest是Python标准库中一个强大而独特的测试工具。它不像unittest或pytest那样需要你专门编写测试类和测试函数。它的工作方式是,扫描你的模块、函数、类甚至普通文本文件,寻找那些看起来像交互式Python会话(即以>>>开头的行)的文本块,然后执行这些代码,并验证输出是否与文档中写下的预期输出完全一致。简单来说,它把你的文档示例变成了自动化测试用例。
我最初接触doctest时,觉得它有点“玩具”——测试怎么能写在注释里呢?但用了几年之后,我发现它在很多场景下效率奇高。尤其是在快速原型验证、编写教程类文档,或者为一些小型工具库提供“自验证”的文档时,doctest能让你一份付出,两份收获:既有了清晰的文档,又有了基础的回归测试。对于从“小白”到“高手”的Python学习者而言,深入理解doctest不仅能提升代码质量,更能培养一种“文档即代码,代码即测试”的工程思维。接下来,我们就从最基础的用法开始,一步步拆解这个看似简单实则精妙的工具。
2. 核心需求与设计思路解析
2.1 为什么需要 doctest?
在深入技术细节之前,我们先想清楚doctest解决了什么痛点。传统的测试框架要求你创建独立的测试文件(如test_*.py),编写测试类和方法,使用各种断言。这个过程是规范的,但也带来了额外的认知负担和文件切换。对于以下场景,doctest提供了更轻量、更直接的解决方案:
- 文档的时效性维护:代码迭代后,文档中的示例很容易过时。
doctest强制要求文档中的示例是可运行的,并且结果正确,这从根本上保证了示例的有效性。 - 快速的功能验证:在开发一个函数时,你可能会在交互式环境(如IPython或Python Shell)里反复试验。
doctest允许你直接将这段交互会话复制粘贴到docstring中,稍作整理就成了一个现成的测试用例。 - 教程与示例驱动开发:如果你在编写一个教程或库的快速入门指南,
doctest能确保你文中的每一个代码片段都是可执行的,读者可以放心地复制粘贴并得到相同的结果,这极大地提升了教程的可信度。 - 轻量级模块自测试:对于一些小型脚本或工具模块,专门搭建一套
unittest或pytest框架可能显得“杀鸡用牛刀”。在模块末尾加几行doctest.testmod()调用,就能实现基本的自检,非常便捷。
doctest的设计哲学是“约定优于配置”和“最小侵入性”。它不要求你学习一套新的API,你只需要会写Python交互式会话就行。这种设计使得它的学习曲线非常平缓。
2.2 doctest 的核心工作机制
理解doctest如何工作,是高效使用它的关键。它的工作流程可以概括为“查找-解析-执行-比对”四步:
- 查找(Finding):
doctest会扫描指定的目标(一个模块、一个类、一个函数,或一个文本文件),寻找所有docstring(文档字符串)。在模块级别,它还会查找一个名为__test__的特殊字典,这个字典可以包含非docstring的测试字符串。 - 解析(Parsing):对于找到的每一个docstring,
doctest使用一个DocTestParser来解析其中的内容。它会识别出以>>>或...(用于续行)开头的“交互式示例”行。一个示例由三部分组成:源代码(>>>后面的部分)、预期输出(紧接着源代码的、不以>>>或...开头的行),以及可选的异常信息。 - 执行(Execution):
doctest为每一组测试(通常是一个docstring)创建一个独立的、干净的命名空间(通常是模块全局变量的一个浅拷贝)。然后,它依次执行示例中的源代码。执行过程中的标准输出(stdout)和发生的异常会被捕获。 - 比对(Checking):将执行得到的实际输出(或异常信息)与文档中写下的预期输出进行逐字比较。默认情况下,这种比较是严格的、精确的。任何细微的差别(包括空格、换行)都会导致测试失败。
这个流程听起来简单,但其中有很多细节和陷阱。例如,如何处理输出中的随机性(如对象内存地址、集合元素的顺序)?如何比较浮点数的近似相等?如何忽略输出中的某些可变部分?doctest通过一系列选项标志(Option Flags)和指令(Directives)来解决这些问题,这也是其灵活性和强大之处。
3. 从入门到精通:四种核心使用模式详解
3.1 模式一:模块内联测试(最常用)
这是doctest最经典、最简单的用法。直接将测试用例写在函数、类或模块的docstring里,然后在模块底部调用doctest.testmod()。
我们以一个计算斐波那契数列的函数为例:
# fib.py def fibonacci(n): """ 返回第n个斐波那契数。 参数: n (int): 一个非负整数。 返回: int: 第n个斐波那契数。 示例: >>> fibonacci(0) 0 >>> fibonacci(1) 1 >>> fibonacci(5) 5 >>> fibonacci(10) 55 >>> fibonacci(-1) Traceback (most recent call last): ... ValueError: n 必须是非负整数 >>> fibonacci(3.5) Traceback (most recent call last): ... ValueError: n 必须是整数 """ if not isinstance(n, int): raise ValueError("n 必须是整数") if n < 0: raise ValueError("n 必须是非负整数") if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b if __name__ == "__main__": import doctest doctest.testmod(verbose=True) # 使用 verbose 模式查看详情操作与解析:
- 我们在
fibonacci函数的docstring中,使用>>>开始了五个交互式示例。前四个是正常的函数调用和预期结果。第五个和第六个是测试异常情况。 - 注意异常测试的写法:我们写下了
Traceback (most recent call last):这一行(这是Python标准异常信息的头),然后用...省略了中间的堆栈跟踪细节(doctest对异常堆栈的匹配非常灵活),最后写上了我们期望的异常类型和详细信息ValueError: n 必须是非负整数。 - 在模块底部,我们加入了
if __name__ == "__main__":这个经典守卫。当这个文件被直接运行时(python fib.py),它会导入doctest模块并调用doctest.testmod()。 testmod()函数会扫描当前模块(fib)中所有对象的docstring,找到我们写的示例并执行它们。- 我们传入了
verbose=True参数,这会让doctest打印出每个测试用例的执行详情。如果不传,则只有在测试失败时才会有输出。
运行与输出: 在命令行中执行python fib.py,你会看到类似下面的详细输出,表明所有测试都通过了:
Trying: fibonacci(0) Expecting: 0 ok Trying: fibonacci(1) Expecting: 1 ok ... 1 items passed all tests: 6 tests in __main__.fibonacci 6 tests in 1 items. 6 passed and 0 failed. Test passed.实操心得:
verbose参数的使用时机在开发调试阶段,强烈建议使用verbose=True或通过命令行python -m doctest -v your_module.py来运行。这能让你清晰地看到每一个测试用例是否按预期执行。但在持续集成(CI)或自动化脚本中,通常使用默认的非详细模式,只有失败时才会输出信息,保持日志的整洁。
3.2 模式二:独立文本文件测试
有时,测试用例更适合放在独立的文本文件中,而不是代码的docstring里。例如,你可能在写一个教程(.rst或.md文件),或者测试用例非常长,放在docstring里会影响代码可读性。doctest.testfile()函数就是为此而生。
假设我们有一个example.txt文件,内容如下:
这是一个关于 `fibonacci` 函数的教程文档。 ========================================== 首先,我们需要导入函数: >>> from fib import fibonacci 基础用法: ---------- 计算前几个斐波那契数: >>> for i in range(5): ... print(fibonacci(i)) 0 1 1 2 3 边界情况测试: ------------- 输入负数会引发错误: >>> fibonacci(-5) Traceback (most recent call last): ... ValueError: n 必须是非负整数然后,我们可以创建一个单独的Python脚本(比如run_doctest.py)来运行这个文本文件中的测试:
# run_doctest.py import doctest if __name__ == "__main__": # 测试同目录下的 example.txt 文件 doctest.testfile("example.txt", verbose=True)操作与解析:
doctest.testfile(“example.txt”)会读取example.txt文件,将其内容视为一个巨大的docstring,并从中提取和执行所有>>>示例。- 文本文件中的测试与模块内联测试完全等价。
doctest会为这个文件创建一个独立的执行上下文。 - 这种方式非常灵活,测试文件可以是任何纯文本格式,不限于
.txt,也可以是.rst(reStructuredText)或.md(Markdown)文件,只要其中包含>>>格式的代码块。
命令行快捷方式: 你甚至不需要写这个Python脚本,可以直接使用命令行模块来运行:python -m doctest -v example.txt。doctest模块会根据文件后缀自动判断使用testfile()逻辑。
注意事项:执行上下文(命名空间)文本文件测试的执行上下文默认是空的。这意味着,在
example.txt中,第一行就是>>> from fib import fibonacci。如果fib模块不在Python路径中,测试会失败。你需要确保测试运行时,必要的模块可以被导入。可以通过globs参数向测试上下文注入全局变量,或者使用package参数来处理相对导入。
3.3 模式三:与 unittest 框架集成
对于大型项目,你可能已经使用了unittest框架来组织测试。doctest可以与unittest无缝集成,让你能够在unittest的测试发现和运行机制中执行doctest。
doctest提供了两个函数来创建unittest.TestSuite:DocTestSuite()和DocFileSuite()。
集成模块测试: 假设我们想为fib.py模块创建一个unittest测试套件。
# test_fib_doctest.py import unittest import doctest import fib # 导入被测试的模块 def load_tests(loader, tests, ignore): """unittest 测试发现协议使用的钩子函数。""" # 将 fib 模块中的所有 doctest 添加到测试套件中 tests.addTests(doctest.DocTestSuite(fib)) return tests # 也可以直接创建 TestSuite(传统方式) suite = doctest.DocTestSuite(fib) if __name__ == '__main__': # 使用 unittest 的 TextTestRunner 运行 runner = unittest.TextTestRunner(verbosity=2) runner.run(suite)集成文本文件测试:
# test_tutorial_doctest.py import unittest import doctest def load_tests(loader, tests, ignore): # 将 example.txt 文件中的 doctest 添加到测试套件中 tests.addTests(doctest.DocFileSuite('example.txt')) return tests操作与解析:
doctest.DocTestSuite(module):从指定模块(或模块名)中提取所有docstring中的测试,并将其包装成unittest.TestCase的子类。doctest.DocFileSuite(*paths):从一个或多个文本文件中提取测试,同样包装成unittest测试用例。- 定义了
load_tests函数后,你可以使用python -m unittest discover命令自动发现并运行这些doctest测试,它们会和你其他的unittest测试用例一起运行,并生成统一的测试报告。 - 这种集成方式让你可以统一使用
unittest的测试运行器、断言方法、setup/teardown机制,同时享受doctest编写测试的便捷。
经验技巧:控制测试范围
DocTestSuite默认会搜索模块中所有对象的docstring。如果你只想测试特定的函数或类,可以通过__test__字典来精确控制。在模块中定义__test__ = {‘test_fib’: fibonacci.__doc__},那么DocTestSuite就只会测试fibonacci函数的docstring。
3.4 模式四:使用test字典组织非文档测试
doctest主要扫描docstring,但有时我们想测试一些东西,又不想污染正式的文档。或者,我们想将测试用例分组、命名。这时就可以使用模块级的__test__字典。
# advanced_fib.py def fibonacci(n): # ... 实现同上,但docstring里不放测试用例 ... pass def _private_helper(x): # 一个内部函数,我们想测试它,但不想暴露在help()中 return x * 2 # 使用 __test__ 字典来组织测试 __test__ = { # 键值对:键是测试集名称,值可以是字符串(包含测试用例)或函数/类对象(从中提取docstring) 'basic_fib': """ >>> from advanced_fib import fibonacci >>> fibonacci(0) 0 >>> fibonacci(5) 5 """, 'error_cases': """ >>> from advanced_fib import fibonacci >>> fibonacci(-1) Traceback (most recent call last): ... ValueError: n 必须是非负整数 """, # 值也可以是一个函数对象,doctest会去解析它的docstring 'test_helper': _private_helper, # 假设 _private_helper 有docstring测试 } if __name__ == "__main__": import doctest doctest.testmod()操作与解析:
__test__字典的每个键值对定义了一组测试。键(如’basic_fib’)会成为测试报告中的标识符(显示为__test__.basic_fib)。- 值可以是字符串(直接包含测试用例),也可以是函数、类或模块对象(
doctest会递归地搜索这些对象的docstring来寻找测试)。 - 当运行
doctest.testmod()时,它会自动发现并运行__test__字典中定义的所有测试。 - 这种方式非常灵活,它允许你将测试代码与正式的API文档分离,保持docstring的简洁性,同时又能进行全面的测试覆盖。
4. 高级特性与实战避坑指南
掌握了基本用法后,你会很快遇到一些现实问题:输出中的对象地址每次都不一样怎么办?浮点数计算有微小误差怎么处理?输出太长想换行怎么办?doctest提供了一套丰富的选项标志(Option Flags)和行内指令(Directives)来解决这些问题。
4.1 核心选项标志(Option Flags)详解
选项标志可以通过doctest.testmod(optionflags=...)或doctest.testfile(optionflags=...)全局设置,也可以通过行内指令为单个测试用例设置。
| 标志常量 | 作用 | 常用场景 |
|---|---|---|
doctest.ELLIPSIS | 允许在预期输出中使用...来匹配任意子串(包括空串和跨行)。 | 匹配对象地址 (<object at 0x...>)、忽略输出中间部分。 |
doctest.NORMALIZE_WHITESPACE | 将所有空白字符序列(空格、换行)视为等价。 | 当输出格式(如列表换行)不重要时,忽略空白差异。 |
doctest.IGNORE_EXCEPTION_DETAIL | 忽略异常详细信息(模块路径、具体消息)的匹配,只检查异常类型。 | 当异常消息可能因Python版本或环境变化时。 |
doctest.DONT_ACCEPT_TRUE_FOR_1 | 禁止将True/False匹配为1/0。 | 需要严格区分布尔值和整数的场景。 |
doctest.DONT_ACCEPT_BLANKLINE | 禁止使用<BLANKLINE>来匹配空行。 | 极少使用,通常保留默认行为。 |
doctest.SKIP | 跳过该示例不执行。 | 用于文档中展示但不想运行的代码(如随机输出)。 |
doctest.FAIL_FAST | 遇到第一个失败就停止,不继续运行后续测试。 | 调试时快速定位第一个问题。 |
doctest.REPORT_NDIFF | 失败时使用difflib.ndiff显示差异,能标记行内不同。 | 需要精细对比输出差异时(默认)。 |
doctest.REPORT_CDIFF/REPORT_UDIFF | 失败时使用上下文差异或统一差异格式显示。 | 根据个人喜好选择差异显示风格。 |
全局使用示例:
import doctest # 组合使用多个标志:启用 ELLIPSIS 和 NORMALIZE_WHITESPACE optionflags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE doctest.testmod(optionflags=optionflags)4.2 行内指令(Directives)的妙用
行内指令更精细,它只影响其所在的那一个示例。语法是在包含>>>的代码行后添加一个注释:# doctest: +FLAG1, -FLAG2。+表示启用,-表示禁用。
实战案例1:处理对象地址和随机输出
def get_unique_object(): """返回一个新对象。 >>> obj = get_unique_object() >>> obj # doctest: +ELLIPSIS <object at 0x...> """ return object() def get_random_list(): """返回一个随机排序的列表(仅用于演示,非真正随机)。 >>> get_random_list() # doctest: +SKIP [3, 1, 4, 1, 5] """ import random lst = [1, 1, 3, 4, 5] random.shuffle(lst) return lst+ELLIPSIS让0x...可以匹配任意十六进制地址。+SKIP让这个示例在测试时被跳过,因为它每次输出都不同。
实战案例2:处理浮点数精度和输出格式
def calculate_pi_approx(): """计算圆周率的近似值。 >>> calculate_pi_approx() # doctest: +ELLIPSIS 3.14159... # 或者,更精确地控制输出格式 >>> result = calculate_pi_approx() >>> round(result, 5) # 在测试代码中处理精度 3.14159 """ return 3.141592653589793对于浮点数,更好的做法是在测试代码内部进行舍入或格式化,而不是依赖ELLIPSIS进行模糊匹配。
实战案例3:处理多行输出和空白
def print_matrix(): """打印一个矩阵。 >>> print_matrix() # doctest: +NORMALIZE_WHITESPACE [1, 2, 3] [4, 5, 6] [7, 8, 9] # 实际输出可能是:[1, 2, 3]\n[4, 5, 6]\n[7, 8, 9] # 或者中间有不同数量的空格,NORMALIZE_WHITESPACE 都能匹配。 """ for i in range(1, 10, 3): print([i, i+1, i+2])实战案例4:忽略异常细节
def risky_operation(): """一个可能抛出多种ValueError的操作。 >>> risky_operation() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValueError: ... """ raise ValueError("Invalid input: x must be positive, got -5")+IGNORE_EXCEPTION_DETAIL使得只要抛出的是ValueError异常,测试就能通过,而不关心冒号后面的具体错误信息是什么。这在异常信息可能变化时非常有用。
4.3 常见问题与排查技巧实录
即使掌握了上述技巧,在实际使用中还是会踩坑。下面是我总结的一些典型问题及其解决方法。
问题1:测试通过了,但感觉没运行?
- 现象:运行
python mymodule.py没有任何输出,即使故意写错一个预期输出。 - 原因:
doctest.testmod()默认只在失败时有输出。你可能没有添加-v参数或verbose=True。 - 解决:使用
python -m doctest -v mymodule.py或doctest.testmod(verbose=True)。确保你看到了Trying:和Expecting:的输出。
问题2:输出中的集合(set)顺序导致失败
- 现象:
>>> {‘a‘, ’b‘, ’c‘}的预期输出是{’a‘, ’b‘, ’c‘},但实际输出可能是{’c‘, ’a‘, ’b‘},测试失败。 - 原因:Python的
set是无序的,其repr()输出顺序不确定。 - 解决:
- (推荐)在测试代码中排序:
>>> sorted(my_set)。 - 使用
==比较:>>> my_set == {’a‘, ’b‘, ’c‘}返回True。 - (不推荐)使用
+ELLIPSIS进行模糊匹配,但这可能掩盖其他错误。
- (推荐)在测试代码中排序:
问题3:测试涉及随机性或外部资源(网络、数据库)
- 现象:测试结果不确定,时而成功时而失败。
- 解决:
- 模拟(Mock):使用
unittest.mock模块替换随机数生成器或网络调用,使其返回确定值。这是最专业的方法。 - 设置随机种子:在测试前
random.seed(42),使随机序列固定。 - 使用
+SKIP指令:如果随机性不是测试重点,直接跳过。 - 测试不变性:不测试具体随机值,测试其属性,如
>>> len(random_list) == 5。
- 模拟(Mock):使用
问题4:doctest 在类方法或嵌套函数中找不到测试?
- 现象:在类的方法docstring里写了测试,但
doctest.testmod()没有执行它们。 - 原因:
doctest默认只能自动发现模块顶层和类顶层的函数、方法、类的docstring。对于嵌套在函数内部定义的函数或类,它无法自动发现。 - 解决:将嵌套的函数/类移到模块层或类层,或者使用
__test__字典显式地注册它们。
问题5:导入错误或命名空间问题
- 现象:在文本文件测试或
__test__字符串中,出现NameError: name ‘xxx’ is not defined。 - 原因:每个
doctest示例组(一个docstring或一个__test__条目)有自己独立的命名空间。默认是模块全局变量的一个浅拷贝。在文本文件中,初始命名空间是空的。 - 解决:
- 确保在测试代码中显式导入所需模块(
>>> from mymodule import myfunc)。 - 使用
doctest.testmod(extraglobs={‘myvar’: 42})或doctest.testfile(globs={…})向测试命名空间注入变量。 - 对于文本文件,考虑使用
package参数来处理相对导入。
- 确保在测试代码中显式导入所需模块(
问题6:输出中包含不可打印字符或编码问题
- 现象:输出看起来一样,但测试失败,差异显示可能包含不可见字符。
- 解决:
- 检查是否混用了空格和制表符(Tab)。
doctest默认将输入中的制表符扩展为8个空格,但输出中的制表符保持不变。建议在测试代码和预期输出中全部使用空格。 - 使用
repr()函数来查看输出的精确形式:>>> print(repr(my_output)),然后将repr()的结果作为预期输出。 - 对于涉及中文等非ASCII字符的情况,确保文件编码(UTF-8)正确,并且Python解释器能正确识别。
- 检查是否混用了空格和制表符(Tab)。
5. 性能、局限性及最佳实践
5.1 doctest 的局限性
doctest并非银弹,它有明确的适用边界:
- 不适合复杂逻辑测试:对于需要复杂setup/teardown、模拟(mocking)、参数化测试的场景,
unittest或pytest更合适。 - 测试执行较慢:由于需要启动独立的Python子进程来执行每个测试组(默认行为),对于超大型的测试套件,
doctest可能比unittest慢。 - 错误信息不够友好:当测试失败时,
doctest默认给出的差异报告可能不如pytest的断言信息直观。 - 可能破坏文档美观:为了测试而添加的大量示例和指令,可能会让文档变得冗长,影响阅读体验。
5.2 最佳实践总结
根据我多年的使用经验,遵循以下原则可以让doctest发挥最大价值:
- 目的明确:想清楚你写
doctest的主要目的是什么?是验证文档,还是进行回归测试?如果是前者,示例应简洁、有代表性;如果是后者,可以更全面,但考虑使用__test__字典分离。 - 示例即文档:每个
doctest示例都应该是高质量的文档。它应该展示函数的典型用法、边界情况和错误处理。避免写一些晦涩难懂、只为测试而测试的例子。 - 保持简洁:一个docstring中的示例不宜过多。如果超过5个,考虑是否应该拆分成多个函数,或者将复杂的测试用例移到
__test__字典或独立的测试文件中。 - 善用选项和指令:熟练掌握
ELLIPSIS,NORMALIZE_WHITESPACE,IGNORE_EXCEPTION_DETAIL等标志,优雅地处理输出可变部分。但也要避免过度使用ELLIPSIS导致测试过于宽松而失去意义。 - 与专业测试框架结合:在大型项目中,将
doctest作为补充,而不是唯一的测试手段。用doctest保证基础示例的正确性,用pytest/unittest进行更全面、更复杂的集成测试和单元测试。 - 在CI中运行:将
doctest纳入你的持续集成(CI)流程。可以简单地通过python -m doctest your_module.py或python -m pytest --doctest-modules来运行。 - 注意安全:
doctest会执行字符串中的任意代码。永远不要对来自不可信来源的文本文件运行doctest.testfile()。
5.3 一个综合性的项目示例
最后,我们来看一个更贴近真实项目的小例子,它融合了多种技巧:
""" math_utils.py - 数学工具函数库。 此模块使用 doctest 同时作为文档和基础测试。 """ import math __test__ = { ‘statistics_tests’: “”“ 统计相关函数的测试集。 >>> from math_utils import mean, variance >>> data = [1.0, 2.0, 3.0, 4.0, 5.0] >>> mean(data) 3.0 >>> variance(data) # doctest: +ELLIPSIS 2.5 ”“” } def mean(numbers): “”“计算算术平均数。 >>> mean([1, 2, 3, 4, 5]) 3.0 >>> mean([10]) 10.0 >>> mean([]) Traceback (most recent call last): ... ValueError: 数字列表不能为空 ”“” if not numbers: raise ValueError(“数字列表不能为空”) return sum(numbers) / len(numbers) def variance(numbers, ddof=0): “”“计算方差。 Args: numbers: 数字列表。 ddof: 自由度增量 (Delta Degrees of Freedom)。默认为0(总体方差)。 Returns: 方差值。 >>> variance([1, 2, 3, 4, 5]) 2.0 >>> variance([1, 2, 3, 4, 5], ddof=1) # 样本方差 2.5 >>> variance([100, 200, 300]) # doctest: +ELLIPSIS 6666.666... ”“” if len(numbers) - ddof <= 0: raise ValueError(“样本数量必须大于自由度增量”) avg = mean(numbers) return sum((x - avg) ** 2 for x in numbers) / (len(numbers) - ddof) if __name__ == “__main__”: import doctest # 组合标志:启用详细输出、ELLIPSIS、并设置失败快速停止以便调试 flags = doctest.ELLIPSIS | doctest.FAIL_FAST failure_count, test_count = doctest.testmod(optionflags=flags, verbose=True) print(f“\nDoctest 完成。共运行 {test_count} 个测试,失败 {failure_count} 个。”) if failure_count == 0: print(“所有文档测试通过!”)这个例子展示了:
- 使用
__test__字典组织一组相关的测试。 - 在函数docstring中提供清晰的使用示例和异常情况。
- 对浮点数输出使用
+ELLIPSIS指令。 - 在主程序中使用组合标志,并给出清晰的总结信息。
doctest的精髓在于它的简单和直接。它鼓励你写出可执行的文档,让文档和代码同步进化。当你养成了在写文档时就思考如何测试它的习惯,你的代码质量和可维护性自然会提升一个台阶。它不是要替代其他测试框架,而是作为一个轻便的、文档驱动的测试补充,无缝嵌入到你的开发工作流中。从今天开始,试着在你下一个工具的docstring里写几个>>>吧,你会发现这种“文档即测试”的体验,既踏实又高效。
