跳到主要内容

Python 单元测试

单元测试的核心是在受控环境下验证单一行为。这里的“单元”指的是设计边界,而非物理上的单个函数。原则是:测试能表达契约的最小接口,避免在测试代码中重复实现业务逻辑。

import pytest

def normalize_name(raw: str) -> str:
name = raw.strip().casefold()
if not name:
raise ValueError("empty name")
return name

@pytest.mark.parametrize(
("raw", "expected"),
[(" Ada ", "ada"), ("GRACE", "grace")],
)
def test_normalize_name(raw: str, expected: str) -> None:
assert normalize_name(raw) == expected

def test_normalize_name_rejects_empty_text() -> None:
with pytest.raises(ValueError, match="empty"):
normalize_name(" ")

Python 标准库自带 unittest,而 pytest 是第三方框架,提供更简洁的断言、Fixture 机制和参数化支持。两者皆可,但建议在一个项目中统一风格,避免无谓的混用。

高质量测试的结构

  • Arrange(准备):只初始化与当前场景直接相关的状态。
  • Act(执行):通过公共接口或明确稳定的边界调用被测代码。
  • Assert(断言):验证可观察的结果、状态变更、发出的事件或预期的失败。
  • 命名:测试函数名应清晰反映“行为 + 条件”。

当多个输入遵循同一契约时,使用参数化(Parametrization)非常高效。如果不同用例的初始化逻辑、预期行为或失败排查方式差异较大,则应拆分为独立测试。注意:参数值可能是可变对象,pytest 不会在测试用例间自动深拷贝它们。

Fixture 与隔离性

Fixture 负责可复用资源的初始化与清理。作用域(Scope)应尽可能小;共享的可变 Session 状态会导致测试间耦合,使失败结果依赖执行顺序。

涉及文件操作时,优先使用 pytest 提供的 tmp_path 等临时目录功能,严禁直接写入代码仓库或用户真实配置目录。

若测试涉及时间、随机数、环境变量或外部 I/O,必须确保其可控,否则结果将不可复现。固定随机种子(Seed)虽可复现,但仅覆盖特定样本;除非有意遍历多种种子,否则无法保证全面性。

测试替身(Test Doubles)

Fake、Stub、Spy 和 Mock 用于替换协作对象,但目的各异。打补丁(Patch)的位置应在名称被引用的地方,而非原始定义处。如果一个小巧的 Fake 实现或注入的可调用对象能更清晰地表达契约,优先使用它们。

过度 Mock 会导致测试仅验证私有调用序列,失去意义。对于数据库、HTTP 客户端、文件系统、序列化及子进程等关键边界,务必保留真实的集成测试。

稳健的断言

断言应聚焦于语义事实,而非偶然的顺序、格式、时间戳或完整对象快照(除非这些是公开契约的一部分)。既要检查返回值,也要检查关键副作用。

对于异常处理,断言异常类型及关键错误信息,避免硬编码不稳定的完整错误消息。

测试必须具备“可失败性”,即能捕获预期的回归。修复 Bug 时,若条件允许,应先运行新测试确认其在旧代码上失败,再应用修复,以确保测试确实覆盖了该缺陷。

运行示例

将第一个代码块保存为 test_names.py。在已经安装 pytest 的环境中运行 python -m pytest -q test_names.py,即可检查两组规范化输入和一组拒绝输入。实际项目应从实现模块导入 normalize_name,不要在测试文件中复制实现。示例只约定去掉首尾空白并折叠大小写,不是完整的人名校验器。

不使用 pytest 时,可以用标准库 unittest 检查相同行为:

import unittest

class NameTests(unittest.TestCase):
def test_normalization(self):
self.assertEqual(normalize_name(" Ada "), "ada")

def test_empty(self):
with self.assertRaises(ValueError):
normalize_name(" ")

将这个类和函数定义放入 test_names.py,不要包含 pytest 的导入和测试,然后运行 python -m unittest -v test_names。测试发现机制会查找 TestCase 子类中以 test 开头的方法;仅定义测试不会自动执行它们。

参考资源

探索关联打开关联网络