首页 / Python3 入门教程 / 文档测试与测试实践

Python3 入门教程

文档测试与测试实践

本教程共 70 篇 · 第 42 篇 · 更新于 2026-07-22 · 约 5 分钟阅读

PythonPython3 入门教程doctest文档测试测试覆盖率TDD

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)是一种开发方法论,核心流程是「红-绿-重构」:

  1. :先写一个测试,运行,它会失败(因为功能还没实现)。
  2. 绿:写最少量的代码让测试通过。
  3. 重构:优化代码结构,保持测试通过。

举个例子,你要实现一个 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 是「先写测试再实现」的开发方法,适合接口稳定的模块。
  • unittestdoctest 可以共存,按需选择。

测试不是负担,而是投资。前期多花一小时写测试,后期能省下十小时的调试时间。代码会变化,需求会变化,但测试给你的信心是稳定的。


来源:参考了 liaoxuefeng「文档测试」、runoob「Python3 错误和异常」等,改写后所得。