单元测试是什么
单元测试针对函数、类或模块等最小可测单元,隔离数据库、网络等外部依赖,用自动化断言快速验证其行为是否符合预期。它处在测试金字塔底层:数量最多、执行最快(毫秒级)、失败定位最直接,通常由开发者随代码一起编写与维护。与集成测试关注模块之间能否协同、端到端测试关注整条业务链路是否可用不同,单元测试聚焦这一小段逻辑本身对不对,因此成为重构与持续集成的第一道安全网。
基本信息
| 测试对象 | 单个函数/类/模块的最小行为单元 |
|---|---|
| 执行特征 | 毫秒级、可每次提交运行、失败直指具体逻辑 |
| 运行频率 | 本地保存即跑、每次提交的 CI 阶段必跑 |
| 依赖处理 | 用测试替身隔离数据库、网络、时间等外部依赖 |
| 常见框架 | pytest(Python)、JUnit 5(Java)、Vitest/Jest(JS)、NUnit(.NET) |
| 编写角色 | 通常由开发者在编码期同步编写 |
| 组织方式 | 与被测代码同仓库,按目录或命名约定区分 |
典型使用场景
- 验证分支逻辑:边界值、异常路径等手工难以穷举的组合,用断言逐条固化
- 重构安全网:改动实现后立刻跑一遍,确认外部行为未被破坏
- TDD 红绿循环:先写失败用例再写实现,把需求拆成可执行的小步
- 缺陷回归:修 bug 前先补一条能复现的失败用例,防止回归复发
- CI 快速门禁:每次提交运行,几分钟内拦住大部分低级错误
- 行为文档:用例名与断言本身就是代码可读的行为说明
pytest 示例
pytest 示例
# 被测逻辑:取款,余额不足时抛异常
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFunds()
return balance - amount
# 正常路径:余额充足
def test_withdraw_reduces_balance():
assert withdraw(100, 30) == 70
# 边界:刚好取完全部余额
def test_withdraw_exact_balance():
assert withdraw(100, 100) == 0
# 异常路径:透支被拒绝且抛出明确异常
def test_withdraw_rejects_overdraft():
with pytest.raises(InsufficientFunds):
withdraw(100, 150)与其他层级对比
| 维度 | 单元测试 | 集成测试 | 端到端测试 |
|---|---|---|---|
| 范围 | 单函数/类 | 模块间与外部依赖 | 整条业务链路 |
| 速度 | 毫秒级 | 秒到十秒级 | 十秒到分钟级 |
| 失败定位 | 直指具体逻辑 | 需判断接口与数据 | 需逐层排查 |
| 维护成本 | 低 | 中 | 高且易碎 |
| 投入占比 | 金字塔底层最多 | 适量 | 仅关键链路 |
一个用例只验证一个行为,按准备-执行-断言三段式组织;断言要具体到返回值、异常类型与关键副作用,避免不报错就算过的写法。
对外依赖用 mock/stub/fake 隔离(见测试替身条目),但不要过度:替身过多时测试会与被测实现细节耦合,重构后大量用例失效,反而拖慢迭代。
参数化可把同类输入的多个组合收敛为一个用例(pytest.mark.parametrize),既压缩重复代码又保留每条输入独立的结果;同类输入的边界取舍见边界值分析条目。
衡量单元测试健康度可看四个信号:套件总时长、失败率、断言密度、缺陷修复是否伴随新用例;这些比覆盖率数字更接近真实质量。
用例应独立可重复:连跑两次结果一致、换机器一致、与执行顺序无关;做到这一点通常要求把时钟、随机数改为显式注入再断言。