🧪 Python 文档测试(doctest)超实用入门

你有没有遇到过这种「尴尬时刻」?写了一堆精美的文档示例,要么自己后来改代码忘同步,要么用户照着敲一行就报错;要么为了验证示例单独写单元测试,又嫌重复麻烦。

别慌!Python 自带的 doctest 模块 能完美解决这个问题——直接把测试用例嵌进代码的文档字符串(docstring)里,看起来就像 Python 交互式解释器的真实操作,既能当文档看,又能一键跑测试,一举两得!


为什么选择 doctest?

doctest 作为轻量级测试工具,有这几个不可替代的优势:

  1. ✅ 自文档化代码:最好的教程就是「可运行的代码」,不用再单独维护文档示例和测试代码
  2. 🔄 强制文档同步:代码改了,示例必须同步,不然直接测试失败
  3. 📦 零依赖开箱即用:Python 内置,不需要安装 pytest/unittest 这类第三方库
  4. 🤝 无缝对接文档工具:Sphinx、MkDocs 都能自动提取这些示例,生成专业的 API 文档

🔧 基础玩法:三步上手

1. 在函数/类的 docstring 里写「仿真对话」

doctest 的测试用例格式很简单:

  • >>> 开头写测试输入
  • 下一行写预期输出(没有输出的话可以空着,但输入/输出的顺序要和真实交互一致)
  • 测试异常时,保留 traceback 的首尾行,中间用 ... 省略

① 简单函数测试

比如重写一个带文档和测试的绝对值函数:

def my_abs(n):
    """
    返回数字的绝对值
    
    日常使用示例:
    >>> my_abs(1)
    1
    >>> my_abs(-100)
    100
    >>> my_abs(0)  # 边界值也测一测
    0
    """
    return n if n >= 0 else (-n)

② 带异常的类测试

再写个支持「属性访问字典值」的小类:

class AttrDict(dict):
    """
    支持 `.key` 和 `['key']` 两种访问方式的字典
    
    常见操作示例:
    >>> ad = AttrDict(name='Bob', age=25)
    >>> ad.name  # 属性访问
    'Bob'
    >>> ad['age']  # 普通字典访问
    25
    >>> ad.city = 'Beijing'  # 属性赋值
    >>> ad['city']
    'Beijing'
    
    异常处理示例:
    >>> ad.nonexistent_key  # 访问不存在的键要报错
    Traceback (most recent call last):
        ...
    AttributeError: 'AttrDict' object has no attribute 'nonexistent_key'
    """
    
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"'AttrDict' object has no attribute '{key}'")
    
    def __setattr__(self, key, value):
        self[key] = value

2. 两种方式运行 doctest

方式一:在模块内部调用(适合快速测试)

在 Python 文件的最底部加上这段代码:

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)  # verbose=True 会显示详细的测试过程,不加就是「无输出即通过」

然后直接用命令行运行文件:

python my_module.py

方式二:命令行直接调用(适合批量/临时测试)

不需要修改文件,直接用 -m 参数调用 doctest:

# 测试单个文件
python -m doctest my_module.py -v

# 测试文件后自动删除临时文件(虽然doctest一般不会留)
python -m doctest my_module.py --clean

🚀 进阶技巧:处理特殊情况

1. 动态/不可预测的输出?用通配符/省略

比如内存地址、当前时间、Python 版本这类每次运行都会变的内容,用 # doctest: +ELLIPSIS 标记,配合 ... 代替不确定的部分:

import datetime
import sys

def get_current_info():
    """
    返回当前年份和 Python 主版本号
    
    示例:
    >>> 2020 <= get_current_info()[0] <= 2100  # 只要年份在合理范围就行
    True
    >>> get_current_info()[1]  # doctest: +ELLIPSIS
    3...
    """
    return (
        datetime.date.today().year,
        sys.version_info.major
    )

2. 完全不需要测试的输出?直接跳过

有些示例(比如展示内存地址、随机数生成的基础用法)根本没法固定预期,用 # doctest: +SKIP 标记,doctest 会直接忽略这条测试:

import random

def get_random_num():
    """
    返回 [0, 1) 之间的随机浮点数
    
    展示用示例(不用测试):
    >>> get_random_num()  # doctest: +SKIP
    0.123456789
    """
    return random.random()

3. 多行输出要对齐吗?要,但只对齐前面的空格!

doctest 对多行输出的要求是:只匹配非空格开头的行,以及前面的空格数量。比如 print() 输出的多行文本,直接原样复制预期输出即可:

def print_greeting(name):
    """
    打印三段式问候语
    
    示例:
    >>> print_greeting('Alice')
    Hello, Alice!
    How are you today?
    Have a nice day!
    """
    print(f"Hello, {name}!")
    print("How are you today?")
    print("Have a nice day!")

🎯 综合实战:阶乘函数的完整测试

让我们把前面学的简单测试、边界测试、异常测试串起来,写一个完整的阶乘函数:

def factorial(n):
    """
    计算正整数的阶乘(n! = 1×2×…×n)
    
    示例:
    >>> factorial(1)  # 最小正整数边界
    1
    >>> factorial(5)  # 常规值测试
    120
    >>> factorial(10)  # 稍大一点的数
    3628800
    
    异常测试:
    >>> factorial(0)  # 非法值1(0不是正整数)
    Traceback (most recent call last):
        ...
    ValueError: n must be a positive integer
    >>> factorial(-3)  # 非法值2(负数不是正整数)
    Traceback (most recent call last):
        ...
    ValueError: n must be a positive integer
    """
    if not isinstance(n, int) or n < 1:
        raise ValueError("n must be a positive integer")
    if n == 1:
        return 1
    return n * factorial(n - 1)

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)

💡 最佳实践

  1. ✅ 保持测试小而精:每个 >>> 块只测一个功能点,不要一次测一堆
  2. ⚠️ 一定要测边界值:比如最小值、最大值、空值、特殊字符
  3. ❌ 避免测试有副作用的代码:不要在 doctest 里修改全局变量、读写磁盘文件、连接数据库——会污染环境,也容易导致测试不稳定
  4. 🤝 和单元测试搭配使用:doctest 适合简单的「文档示例验证」,复杂的业务逻辑、性能测试还是要交给 pytest/unittest
  5. 📝 尽量写有意义的示例:不要只测 1+1,要测用户实际会用到的场景

📚 对接文档生成工具

doctest 最大的优势之一就是能和 Sphinx、MkDocs Material 这类主流工具无缝配合:

Sphinx 配置

在 Sphinx 的 conf.py 里启用 sphinx.ext.doctest 扩展:

extensions = [
    'sphinx.ext.doctest',  # 核心扩展
    'sphinx.ext.autodoc',  # 自动提取 docstring
    # 其他扩展...
]

然后在项目根目录运行:

make doctest

这样不仅会测代码里的 docstring,连 .rst 文档里手动写的 >>> 块也能一起批量测试!


🎬 总结

Python 的 doctest 是一个「性价比极高」的工具——

  • 不需要额外学习复杂的测试语法
  • 能同时解决「文档示例同步」和「轻量级测试」两个问题
  • 还能和专业文档工具联动

现在就给你的代码加几个 doctest 试试吧!再也不怕文档示例坑自己坑用户啦~