Python doctest模块详解:让文档成为可执行的自动化测试

📅 2026/7/15 8:29:20 👁️ 阅读次数 📝 编程学习
Python doctest模块详解:让文档成为可执行的自动化测试

1. 项目概述

如果你写过Python代码,尤其是写过一些库或者工具函数,大概率会在函数的docstring(文档字符串)里写几个使用示例。比如,你写了一个计算阶乘的函数,可能会顺手在文档里写上>>> factorial(5)和预期的120。这些示例代码,对阅读文档的人来说是极好的指引,但对你来说,它们仅仅是“注释”吗?你有没有想过,这些写在注释里的代码,其实可以直接运行,并且验证其结果是否正确?这就是doctest模块的核心思想:让文档成为可执行的测试

doctest是Python标准库中一个强大而独特的测试工具。它不像unittestpytest那样需要你专门编写测试类和测试函数。它的工作方式是,扫描你的模块、函数、类甚至普通文本文件,寻找那些看起来像交互式Python会话(即以>>>开头的行)的文本块,然后执行这些代码,并验证输出是否与文档中写下的预期输出完全一致。简单来说,它把你的文档示例变成了自动化测试用例。

我最初接触doctest时,觉得它有点“玩具”——测试怎么能写在注释里呢?但用了几年之后,我发现它在很多场景下效率奇高。尤其是在快速原型验证、编写教程类文档,或者为一些小型工具库提供“自验证”的文档时,doctest能让你一份付出,两份收获:既有了清晰的文档,又有了基础的回归测试。对于从“小白”到“高手”的Python学习者而言,深入理解doctest不仅能提升代码质量,更能培养一种“文档即代码,代码即测试”的工程思维。接下来,我们就从最基础的用法开始,一步步拆解这个看似简单实则精妙的工具。

2. 核心需求与设计思路解析

2.1 为什么需要 doctest?

在深入技术细节之前,我们先想清楚doctest解决了什么痛点。传统的测试框架要求你创建独立的测试文件(如test_*.py),编写测试类和方法,使用各种断言。这个过程是规范的,但也带来了额外的认知负担和文件切换。对于以下场景,doctest提供了更轻量、更直接的解决方案:

  1. 文档的时效性维护:代码迭代后,文档中的示例很容易过时。doctest强制要求文档中的示例是可运行的,并且结果正确,这从根本上保证了示例的有效性。
  2. 快速的功能验证:在开发一个函数时,你可能会在交互式环境(如IPython或Python Shell)里反复试验。doctest允许你直接将这段交互会话复制粘贴到docstring中,稍作整理就成了一个现成的测试用例。
  3. 教程与示例驱动开发:如果你在编写一个教程或库的快速入门指南,doctest能确保你文中的每一个代码片段都是可执行的,读者可以放心地复制粘贴并得到相同的结果,这极大地提升了教程的可信度。
  4. 轻量级模块自测试:对于一些小型脚本或工具模块,专门搭建一套unittestpytest框架可能显得“杀鸡用牛刀”。在模块末尾加几行doctest.testmod()调用,就能实现基本的自检,非常便捷。

doctest的设计哲学是“约定优于配置”和“最小侵入性”。它不要求你学习一套新的API,你只需要会写Python交互式会话就行。这种设计使得它的学习曲线非常平缓。

2.2 doctest 的核心工作机制

理解doctest如何工作,是高效使用它的关键。它的工作流程可以概括为“查找-解析-执行-比对”四步:

  1. 查找(Finding)doctest会扫描指定的目标(一个模块、一个类、一个函数,或一个文本文件),寻找所有docstring(文档字符串)。在模块级别,它还会查找一个名为__test__的特殊字典,这个字典可以包含非docstring的测试字符串。
  2. 解析(Parsing):对于找到的每一个docstring,doctest使用一个DocTestParser来解析其中的内容。它会识别出以>>>...(用于续行)开头的“交互式示例”行。一个示例由三部分组成:源代码>>>后面的部分)、预期输出(紧接着源代码的、不以>>>...开头的行),以及可选的异常信息
  3. 执行(Execution)doctest为每一组测试(通常是一个docstring)创建一个独立的、干净的命名空间(通常是模块全局变量的一个浅拷贝)。然后,它依次执行示例中的源代码。执行过程中的标准输出(stdout)和发生的异常会被捕获。
  4. 比对(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 模式查看详情

操作与解析

  1. 我们在fibonacci函数的docstring中,使用>>>开始了五个交互式示例。前四个是正常的函数调用和预期结果。第五个和第六个是测试异常情况。
  2. 注意异常测试的写法:我们写下了Traceback (most recent call last):这一行(这是Python标准异常信息的头),然后用...省略了中间的堆栈跟踪细节(doctest对异常堆栈的匹配非常灵活),最后写上了我们期望的异常类型和详细信息ValueError: n 必须是非负整数
  3. 在模块底部,我们加入了if __name__ == "__main__":这个经典守卫。当这个文件被直接运行时(python fib.py),它会导入doctest模块并调用doctest.testmod()
  4. testmod()函数会扫描当前模块(fib)中所有对象的docstring,找到我们写的示例并执行它们。
  5. 我们传入了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)

操作与解析

  1. doctest.testfile(“example.txt”)会读取example.txt文件,将其内容视为一个巨大的docstring,并从中提取和执行所有>>>示例。
  2. 文本文件中的测试与模块内联测试完全等价。doctest会为这个文件创建一个独立的执行上下文。
  3. 这种方式非常灵活,测试文件可以是任何纯文本格式,不限于.txt,也可以是.rst(reStructuredText)或.md(Markdown)文件,只要其中包含>>>格式的代码块。

命令行快捷方式: 你甚至不需要写这个Python脚本,可以直接使用命令行模块来运行:python -m doctest -v example.txtdoctest模块会根据文件后缀自动判断使用testfile()逻辑。

注意事项:执行上下文(命名空间)文本文件测试的执行上下文默认是空的。这意味着,在example.txt中,第一行就是>>> from fib import fibonacci。如果fib模块不在Python路径中,测试会失败。你需要确保测试运行时,必要的模块可以被导入。可以通过globs参数向测试上下文注入全局变量,或者使用package参数来处理相对导入。

3.3 模式三:与 unittest 框架集成

对于大型项目,你可能已经使用了unittest框架来组织测试。doctest可以与unittest无缝集成,让你能够在unittest的测试发现和运行机制中执行doctest

doctest提供了两个函数来创建unittest.TestSuiteDocTestSuite()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

操作与解析

  1. doctest.DocTestSuite(module):从指定模块(或模块名)中提取所有docstring中的测试,并将其包装成unittest.TestCase的子类。
  2. doctest.DocFileSuite(*paths):从一个或多个文本文件中提取测试,同样包装成unittest测试用例。
  3. 定义了load_tests函数后,你可以使用python -m unittest discover命令自动发现并运行这些doctest测试,它们会和你其他的unittest测试用例一起运行,并生成统一的测试报告。
  4. 这种集成方式让你可以统一使用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()

操作与解析

  1. __test__字典的每个键值对定义了一组测试。键(如’basic_fib’)会成为测试报告中的标识符(显示为__test__.basic_fib)。
  2. 值可以是字符串(直接包含测试用例),也可以是函数、类或模块对象doctest会递归地搜索这些对象的docstring来寻找测试)。
  3. 当运行doctest.testmod()时,它会自动发现并运行__test__字典中定义的所有测试。
  4. 这种方式非常灵活,它允许你将测试代码与正式的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
  • +ELLIPSIS0x...可以匹配任意十六进制地址。
  • +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.pydoctest.testmod(verbose=True)。确保你看到了Trying:Expecting:的输出。

问题2:输出中的集合(set)顺序导致失败

  • 现象>>> {‘a‘, ’b‘, ’c‘}的预期输出是{’a‘, ’b‘, ’c‘},但实际输出可能是{’c‘, ’a‘, ’b‘},测试失败。
  • 原因:Python的set是无序的,其repr()输出顺序不确定。
  • 解决
    1. (推荐)在测试代码中排序>>> sorted(my_set)
    2. 使用==比较:>>> my_set == {’a‘, ’b‘, ’c‘}返回True
    3. (不推荐)使用+ELLIPSIS进行模糊匹配,但这可能掩盖其他错误。

问题3:测试涉及随机性或外部资源(网络、数据库)

  • 现象:测试结果不确定,时而成功时而失败。
  • 解决
    1. 模拟(Mock):使用unittest.mock模块替换随机数生成器或网络调用,使其返回确定值。这是最专业的方法。
    2. 设置随机种子:在测试前random.seed(42),使随机序列固定。
    3. 使用+SKIP指令:如果随机性不是测试重点,直接跳过。
    4. 测试不变性:不测试具体随机值,测试其属性,如>>> len(random_list) == 5

问题4:doctest 在类方法或嵌套函数中找不到测试?

  • 现象:在类的方法docstring里写了测试,但doctest.testmod()没有执行它们。
  • 原因doctest默认只能自动发现模块顶层类顶层的函数、方法、类的docstring。对于嵌套在函数内部定义的函数或类,它无法自动发现。
  • 解决:将嵌套的函数/类移到模块层或类层,或者使用__test__字典显式地注册它们。

问题5:导入错误或命名空间问题

  • 现象:在文本文件测试或__test__字符串中,出现NameError: name ‘xxx’ is not defined
  • 原因:每个doctest示例组(一个docstring或一个__test__条目)有自己独立的命名空间。默认是模块全局变量的一个浅拷贝。在文本文件中,初始命名空间是空的。
  • 解决
    1. 确保在测试代码中显式导入所需模块(>>> from mymodule import myfunc)。
    2. 使用doctest.testmod(extraglobs={‘myvar’: 42})doctest.testfile(globs={…})向测试命名空间注入变量。
    3. 对于文本文件,考虑使用package参数来处理相对导入。

问题6:输出中包含不可打印字符或编码问题

  • 现象:输出看起来一样,但测试失败,差异显示可能包含不可见字符。
  • 解决
    1. 检查是否混用了空格和制表符(Tab)。doctest默认将输入中的制表符扩展为8个空格,但输出中的制表符保持不变。建议在测试代码和预期输出中全部使用空格
    2. 使用repr()函数来查看输出的精确形式:>>> print(repr(my_output)),然后将repr()的结果作为预期输出。
    3. 对于涉及中文等非ASCII字符的情况,确保文件编码(UTF-8)正确,并且Python解释器能正确识别。

5. 性能、局限性及最佳实践

5.1 doctest 的局限性

doctest并非银弹,它有明确的适用边界:

  • 不适合复杂逻辑测试:对于需要复杂setup/teardown、模拟(mocking)、参数化测试的场景,unittestpytest更合适。
  • 测试执行较慢:由于需要启动独立的Python子进程来执行每个测试组(默认行为),对于超大型的测试套件,doctest可能比unittest慢。
  • 错误信息不够友好:当测试失败时,doctest默认给出的差异报告可能不如pytest的断言信息直观。
  • 可能破坏文档美观:为了测试而添加的大量示例和指令,可能会让文档变得冗长,影响阅读体验。

5.2 最佳实践总结

根据我多年的使用经验,遵循以下原则可以让doctest发挥最大价值:

  1. 目的明确:想清楚你写doctest的主要目的是什么?是验证文档,还是进行回归测试?如果是前者,示例应简洁、有代表性;如果是后者,可以更全面,但考虑使用__test__字典分离。
  2. 示例即文档:每个doctest示例都应该是高质量的文档。它应该展示函数的典型用法、边界情况和错误处理。避免写一些晦涩难懂、只为测试而测试的例子。
  3. 保持简洁:一个docstring中的示例不宜过多。如果超过5个,考虑是否应该拆分成多个函数,或者将复杂的测试用例移到__test__字典或独立的测试文件中。
  4. 善用选项和指令:熟练掌握ELLIPSIS,NORMALIZE_WHITESPACE,IGNORE_EXCEPTION_DETAIL等标志,优雅地处理输出可变部分。但也要避免过度使用ELLIPSIS导致测试过于宽松而失去意义。
  5. 与专业测试框架结合:在大型项目中,将doctest作为补充,而不是唯一的测试手段。用doctest保证基础示例的正确性,用pytest/unittest进行更全面、更复杂的集成测试和单元测试。
  6. 在CI中运行:将doctest纳入你的持续集成(CI)流程。可以简单地通过python -m doctest your_module.pypython -m pytest --doctest-modules来运行。
  7. 注意安全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(“所有文档测试通过!”)

这个例子展示了:

  1. 使用__test__字典组织一组相关的测试。
  2. 在函数docstring中提供清晰的使用示例和异常情况。
  3. 对浮点数输出使用+ELLIPSIS指令。
  4. 在主程序中使用组合标志,并给出清晰的总结信息。

doctest的精髓在于它的简单和直接。它鼓励你写出可执行的文档,让文档和代码同步进化。当你养成了在写文档时就思考如何测试它的习惯,你的代码质量和可维护性自然会提升一个台阶。它不是要替代其他测试框架,而是作为一个轻便的、文档驱动的测试补充,无缝嵌入到你的开发工作流中。从今天开始,试着在你下一个工具的docstring里写几个>>>吧,你会发现这种“文档即测试”的体验,既踏实又高效。