文档测试与测试实践
本教程共 70 篇 · 第 42 篇 · 更新于 2026-07-22 · 约 5 分钟阅读
42. 文档测试与测试实践
本节目标:学会用
doctest把文档字符串变成测试,了解测试覆盖率和 TDD 的基本思想。
41 章讲了 unittest,适合系统化的测试套件。但如果你只想验证几个简单函数的输入输出,写一整套测试类有点杀鸡用牛刀。
doctest 模块提供了更轻量的方式:把测试用例直接写在函数的文档字符串(docstring)里。
doctest 入门
doctest 会扫描模块里的 docstring,找到类似交互式会话的代码块并执行:
def add(a, b):
"""
返回两个数的和。
>>> add(1, 2)
3
>>> add(-1, 1)
0
>>> add(0.1, 0.2)
0.30000000000000004
"""
return a + b
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True)
>>> 开头的行是要执行的代码,下一行是期望的输出。如果实际输出和期望不符,doctest 会报错。
Tip
doctest的输出必须精确匹配,包括空格和引号。字符串输出用单引号还是双引号,doctest是很挑剔的。如果结果不稳定(比如字典的遍历顺序),用# doctest: +ELLIPSIS做模糊匹配,或改用unittest。
在命令行运行 doctest
不需要在代码里写 doctest.testmod(),可以直接命令行运行:
python -m doctest mymodule.py -v
-v 参数显示详细输出,包括通过了哪些测试。不加 -v 时,只有失败才会打印信息,适合集成到 CI 流程里。
doctest 的适用场景
doctest 最适合以下情况:
- 纯函数,输入输出明确。
- 需要同时写文档和测试,一箭双雕。
- 教学示例、API 文档里的代码片段。
不适合的情况:
- 需要复杂前置条件(数据库、网络)。
- 输出包含随机性、时间戳、内存地址。
- 需要 mock 外部依赖。
doctest 与 unittest 的结合
一个项目里可以同时使用两种测试。简单工具函数用 doctest,复杂业务逻辑用 unittest:
# utils.py
def truncate(text, length):
"""
截断文本到指定长度,超出部分加省略号。
>>> truncate('hello world', 8)
'hello...'
>>> truncate('hi', 8)
'hi'
"""
if len(text) <= length:
return text
return text[:length - 3] + '...'
# test_app.py
import unittest
from app import OrderService
class TestOrderService(unittest.TestCase):
def test_create_order(self):
service = OrderService()
order = service.create(user_id=1, items=[...])
self.assertIsNotNone(order.id)
两者互补,覆盖不同层面的测试需求。
测试覆盖率
写了测试,怎么知道测得够不够?「测试覆盖率(test coverage)」是一个度量指标,表示测试代码执行了被测代码的百分之多少。
Python 用 coverage.py 工具统计覆盖率:
pip install coverage
# 运行测试并收集覆盖率数据
coverage run -m unittest discover
# 生成文本报告
coverage report
# 生成 HTML 报告,在浏览器里查看
coverage html
coverage report 的输出类似这样:
Name Stmts Miss Cover
-----------------------------------
utils.py 15 0 100%
app.py 80 20 75%
-----------------------------------
TOTAL 95 20 79%
Warning覆盖率不是越高越好。100% 覆盖率不代表没有 bug,只代表每行代码都被执行过。关键路径的逻辑分支、边界条件,比单纯的行数覆盖更重要。
TDD:测试驱动开发
TDD(Test-Driven Development)是一种开发方法论,核心流程是「红-绿-重构」:
- 红:先写一个测试,运行,它会失败(因为功能还没实现)。
- 绿:写最少量的代码让测试通过。
- 重构:优化代码结构,保持测试通过。
举个例子,你要实现一个 to_slug 函数,把标题转成 URL 友好的短横线格式:
# 第一步:写测试(红)
import unittest
class TestSlug(unittest.TestCase):
def test_basic(self):
self.assertEqual(to_slug('Hello World'), 'hello-world')
# 运行测试 -> 失败,to_slug 还不存在
# 第二步:实现功能(绿)
def to_slug(title):
return title.lower().replace(' ', '-')
# 运行测试 -> 通过
# 第三步:扩展测试,继续循环
# (演示步骤,实际只需一个类,在上面逐步添加方法即可)
class TestSlug(unittest.TestCase):
def test_basic(self):
self.assertEqual(to_slug('Hello World'), 'hello-world')
def test_multiple_spaces(self):
self.assertEqual(to_slug('A B C'), 'a-b-c')
TDD 的好处是强迫你在写实现之前想清楚接口和行为。缺点是开发速度在初期会变慢,而且对需求频繁变动的项目不太友好。
Note你不必 strict 地遵循 TDD 的每一步。很多开发者采用「混合模式」:对核心算法和稳定接口写 TDD,对 UI 和探索性代码先实现再补测试。找到适合自己的节奏最重要。
测试目录的组织建议
一个中等项目的测试文件可以这样组织:
myproject/
├── src/
│ ├── utils.py
│ └── app.py
├── tests/
│ ├── test_utils.py
│ ├── test_app.py
│ └── __init__.py
└── README.md
- 测试文件统一放
tests/目录。 - 测试文件命名以
test_开头,unittest能自动发现。 - 不要和被测代码混在一起,保持源码目录整洁。
小结
doctest把交互式示例变成自动化测试,适合简单函数和文档。- 覆盖率衡量测试的广度,但不要盲目追求 100%。
- TDD 是「先写测试再实现」的开发方法,适合接口稳定的模块。
unittest和doctest可以共存,按需选择。
测试不是负担,而是投资。前期多花一小时写测试,后期能省下十小时的调试时间。代码会变化,需求会变化,但测试给你的信心是稳定的。
来源:参考了 liaoxuefeng「文档测试」、runoob「Python3 错误和异常」等,改写后所得。