Skip to content

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 会:

  1. 提取 >>> 后面的代码;
  2. 执行它;
  3. 对比实际输出和注释里写的期待输出。

生活化理解:注释是"产品说明书",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:两种测试的对比

维度doctestunittest
位置写在注释里单独文件
可读性极好,用户直接看需要看测试文件
复杂度适合简单示例适合复杂场景
断言方式对比输出文本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,用 ... 匹配堆栈。

八、小结

  1. 文档测试:把示例代码写在注释里,doctest 自动提取执行并验证;
  2. 格式>>> 是输入,无提示符行是期待输出,... 匹配任意内容;
  3. 运行python myfile.py,正确时沉默,错误时报详情;
  4. 保护机制if __name__ == '__main__' 确保模块被导入时不执行测试;
  5. 与 unittest 对比:doctest 简单直观适合文档,unittest 全面严格适合系统测试;
  6. 核心价值文档即测试,测试即文档——用户看文档学会用,开发者跑测试保正确。

doctest 是 Python 的"试用品"——让用户当场验证,让开发者即时确认,一举两得。