Python 文档测试:注释里的"自动验收员"
引言:从"口头承诺"到"白纸黑字"
想象你买家电:
- 口头承诺:销售员说"这冰箱一天一度电"——你信了,回家发现一天三度,找谁说理去?
- 白纸黑字(文档测试):说明书上印着"输入 220V,日耗电 1 度"——你可以当场测试,不符就退货。
Python 的 doctest 就是把"承诺"写在代码注释里,并且能自动验证真伪。
一、什么是文档测试?
1.1 从官方文档说起
Python 官方文档经常这样写:
python
>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'这既是文档(告诉你怎么用),也是测试(可以复制到交互式环境验证)。
1.2 核心思想
把示例代码写在注释里,让工具自动提取并执行,验证结果是否正确。
python
def abs(n):
'''
返回绝对值
>>> abs(1)
1
>>> abs(-1)
1
>>> abs(0)
0
'''
return n if n >= 0 else (-n)注释里的 >>> 就是交互式命令行的提示符,doctest 会:
- 提取
>>>后面的代码; - 执行它;
- 对比实际输出和注释里写的期待输出。
生活化理解:注释是"产品说明书",doctest 是"质检员"——说明书说"按红色按钮出热水",质检员就真去按,看出的是不是热水。
二、实战:给 Dict 类写文档测试
2.1 被测代码
python
# mydict2.py
class Dict(dict):
'''
支持属性访问的字典
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError("'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__ == '__main__':
import doctest
doctest.testmod()2.2 关键语法
| 元素 | 说明 |
|---|---|
>>> | 交互式命令行输入 |
| 无提示符的行 | 期待输出 |
... | 匹配任意内容(用于异常堆栈) |
2.3 运行
bash
python mydict2.py正确时:没有任何输出——沉默即通过。
错误时:把 __getattr__ 注释掉再运行:
**********************************************************************
File "mydict2.py", line 10, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
...
1 items had failures:
2 of 9 in __main__.Dict
***Test Failed*** 2 failures.2.4 为什么用 if __name__ == '__main__'?
python
if __name__ == '__main__':
import doctest
doctest.testmod()作用:模块被 import 时,doctest 不执行;直接运行脚本时才执行——不污染正常使用。
三、doctest vs unittest:两种测试的对比
| 维度 | doctest | unittest |
|---|---|---|
| 位置 | 写在注释里 | 单独文件 |
| 可读性 | 极好,用户直接看 | 需要看测试文件 |
| 复杂度 | 适合简单示例 | 适合复杂场景 |
| 断言方式 | 对比输出文本 | assertEqual 等丰富断言 |
| 异常测试 | 用 ... 匹配堆栈 | assertRaises 精确断言 |
| 适用场景 | 文档 + 简单验证 | 全面、系统的测试 |
生活化理解:
doctest是"产品说明书上的试用装"——简单、直观、当场验证;unittest是"专业质检报告"——全面、严格、覆盖各种极端情况。
四、知识链条:从文档到测试
编写函数/类
↓
写文档字符串(docstring),包含使用示例
↓
示例写成交互式格式(>>> 输入 + 期待输出)
↓
doctest.testmod() 自动提取并执行
↓
输出一致?沉默通过!
↓
输出不一致?报错,定位问题五、常见误区与避坑指南
5.1 误区一:期待输出格式不对
python
>>> print('hello')
hello # ✅ 正确
>>> print('hello')
'hello' # ❌ 错误!print 输出不带引号原则:期待输出必须和实际打印结果完全一致,包括空格、换行。
5.2 误区二:异常堆栈写完整路径
python
>>> d2['empty']
Traceback (most recent call last):
File "C:\Users\test\mydict2.py", line 25, in <module>
...
KeyError: 'empty'问题:不同机器路径不同,测试会失败。
修正:用 ... 匹配任意堆栈内容:
python
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'5.3 误区三:doctest 替代 unittest
python
# ❌ 用 doctest 做复杂测试
>>> result = complex_calculation(data)
>>> len(result) > 100
True
>>> all(x > 0 for x in result)
True问题:复杂逻辑用 doctest 写起来笨重,读起来痛苦。
原则:简单示例用 doctest,系统测试用 unittest。
5.4 误区四:忘记 ... 前的空格
python
Traceback (most recent call last):
... # ❌ 错误!... 前要有空格
...正确:
python
Traceback (most recent call last):
... # ✅ 缩进后的 ...六、实际应用案例
案例 1:数学工具库
python
def factorial(n):
'''
计算 n 的阶乘
>>> factorial(1)
1
>>> factorial(5)
120
>>> factorial(0)
1
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n 必须是非负整数
'''
if n < 0:
raise ValueError('n 必须是非负整数')
if n <= 1:
return 1
return n * factorial(n - 1)
def is_prime(n):
'''
判断是否为质数
>>> is_prime(2)
True
>>> is_prime(7)
True
>>> is_prime(8)
False
>>> is_prime(1)
False
>>> is_prime(0)
False
'''
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True) # verbose=True 显示详细结果verbose 输出:
Trying:
factorial(1)
Expecting:
1
ok
Trying:
factorial(5)
Expecting:
120
ok
...
4 items passed all tests:
4 tests in __main__.factorial
5 tests in __main__.is_prime
9 tests in 2 items.
9 passed and 0 failed.
Test passed.生活化理解:verbose=True 是"质检员逐项打勾",默认是"全部合格才盖章"。
案例 2:字符串处理工具
python
def camel_to_snake(name):
'''
驼峰转蛇形
>>> camel_to_snake('helloWorld')
'hello_world'
>>> camel_to_snake('HTTPServer')
'h_t_t_p_server'
>>> camel_to_snake('')
''
>>> camel_to_snake('already_snake')
'already_snake'
'''
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def truncate(text, max_len, suffix='...'):
'''
截断文本
>>> truncate('hello world', 8)
'hello...'
>>> truncate('short', 10)
'short'
>>> truncate('hello world', 5, suffix='>>')
'hello>>'
'''
if len(text) <= max_len:
return text
return text[:max_len] + suffix
if __name__ == '__main__':
import doctest
doctest.testmod()优势:用户看文档就知道怎么用,开发者改代码后跑一遍就知道有没有改坏。
七、实战练习
练习:给 fact(n) 写文档测试
python
def fact(n):
'''
计算 1 * 2 * ... * n
>>> fact(1)
1
>>> fact(10)
?
>>> fact(-1)
?
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()参考答案
python
def fact(n):
'''
计算 1 * 2 * ... * n
>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()说明:
fact(10) = 3628800,直接写结果;fact(-1)抛ValueError,用...匹配堆栈。
八、小结
- 文档测试:把示例代码写在注释里,
doctest自动提取执行并验证; - 格式:
>>>是输入,无提示符行是期待输出,...匹配任意内容; - 运行:
python myfile.py,正确时沉默,错误时报详情; - 保护机制:
if __name__ == '__main__'确保模块被导入时不执行测试; - 与 unittest 对比:doctest 简单直观适合文档,unittest 全面严格适合系统测试;
- 核心价值:文档即测试,测试即文档——用户看文档学会用,开发者跑测试保正确。
doctest 是 Python 的"试用品"——让用户当场验证,让开发者即时确认,一举两得。