🧪 Python 文档测试(doctest)超实用入门
你有没有遇到过这种「尴尬时刻」?写了一堆精美的文档示例,要么自己后来改代码忘同步,要么用户照着敲一行就报错;要么为了验证示例单独写单元测试,又嫌重复麻烦。
别慌!Python 自带的 doctest 模块 能完美解决这个问题——直接把测试用例嵌进代码的文档字符串(docstring)里,看起来就像 Python 交互式解释器的真实操作,既能当文档看,又能一键跑测试,一举两得!
为什么选择 doctest?
doctest 作为轻量级测试工具,有这几个不可替代的优势:
- ✅ 自文档化代码:最好的教程就是「可运行的代码」,不用再单独维护文档示例和测试代码
- 🔄 强制文档同步:代码改了,示例必须同步,不然直接测试失败
- 📦 零依赖开箱即用:Python 内置,不需要安装 pytest/unittest 这类第三方库
- 🤝 无缝对接文档工具: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 会显示详细的测试过程,不加就是「无输出即通过」
然后直接用命令行运行文件:
方式二:命令行直接调用(适合批量/临时测试)
不需要修改文件,直接用 -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)
💡 最佳实践
- ✅ 保持测试小而精:每个
>>> 块只测一个功能点,不要一次测一堆
- ⚠️ 一定要测边界值:比如最小值、最大值、空值、特殊字符
- ❌ 避免测试有副作用的代码:不要在 doctest 里修改全局变量、读写磁盘文件、连接数据库——会污染环境,也容易导致测试不稳定
- 🤝 和单元测试搭配使用:doctest 适合简单的「文档示例验证」,复杂的业务逻辑、性能测试还是要交给 pytest/unittest
- 📝 尽量写有意义的示例:不要只测
1+1,要测用户实际会用到的场景
📚 对接文档生成工具
doctest 最大的优势之一就是能和 Sphinx、MkDocs Material 这类主流工具无缝配合:
Sphinx 配置
在 Sphinx 的 conf.py 里启用 sphinx.ext.doctest 扩展:
extensions = [
'sphinx.ext.doctest', # 核心扩展
'sphinx.ext.autodoc', # 自动提取 docstring
# 其他扩展...
]
然后在项目根目录运行:
这样不仅会测代码里的 docstring,连 .rst 文档里手动写的 >>> 块也能一起批量测试!
🎬 总结
Python 的 doctest 是一个「性价比极高」的工具——
- 不需要额外学习复杂的测试语法
- 能同时解决「文档示例同步」和「轻量级测试」两个问题
- 还能和专业文档工具联动
现在就给你的代码加几个 doctest 试试吧!再也不怕文档示例坑自己坑用户啦~