单元测试基础
嘿,欢迎来到第2章。今天咱们聊聊单元测试的根基——到底什么是单元测试、测试金字塔长什么样,以及测试驱动开发(TDD)是怎么一回事。这些内容看起来基础,但我在项目里见过太多人一开始就绕弯路,所以咱们把话说透。
什么是单元测试
单元测试,说白了就是针对软件里最小可测试单元的验证。这个“单元”通常是一个函数、一个方法,或者一个类里面的公开行为。它不依赖数据库、网络、文件系统这些外部资源——因为这些该由集成测试去管。
我个人习惯把单元测试比作“显微镜下的质检员”。你写一个函数计算折扣价,单元测试就去检查输入正确的金额能不能返回正确结果,边界值(比如0元、负数、超大量)会不会崩溃。
单元测试的核心特征
- 快速:毫秒级执行,跑完上千个测试也就几秒
- 隔离:不碰外部依赖,用Mock/Stub替代
- 可重复:任何环境、任何时候跑结果都一样
- 自动化:集成到构建流程里,每次提交都自动执行
举个最简例子。假设我们有一个计算税率的函数:
function calculateTax(amount, rate) {
// 金额为0或负数时返回0
if (amount <= 0) return 0;
return amount * rate;
}
它的单元测试长这样(用Jest风格):
test('正常金额计算税', () => {
expect(calculateTax(100, 0.1)).toBe(10);
});
test('金额为0返回0', () => {
expect(calculateTax(0, 0.1)).toBe(0);
});
test('金额为负数返回0', () => {
expect(calculateTax(-50, 0.1)).toBe(0);
});
你看,每一行代码都只有一个验证点。我刚开始写单元测试时,总喜欢在一个用例里测好几个场景,后来发现失败了根本定位不到哪一行——教训啊。
我曾经踩过的坑: 写单元测试时把数据库连上了,结果测试跑了两分钟,整个CI卡住。记住:单元测试必须做到“不落盘、不联网、不跨进程”。
测试金字塔模型
提到测试分层,绕不开这个经典模型——测试金字塔。它由Mike Cohn提出,把测试按粒度从下往上分成三层:
- 单元测试(底层):数量最多,速度最快,覆盖业务逻辑
- 集成测试(中间层):验证模块间的交互,比如API、数据库
- 端到端测试(顶层):模拟真实用户操作,数量最少
金字塔的形状说明了什么?底层要厚实,顶层要薄。我见过一个团队把大量精力花在UI自动化上,结果每次页面改版整个测试套件全崩,反观单位测试覆盖率不到10%——你想想看,这种投资回报率有多低。
下面的表帮你快速对比各层特点:
| 层级 | 粒度 | 执行速度 | 维护成本 | 典型工具 |
|---|---|---|---|---|
| 端到端测试 | 粗(整个系统) | 慢(秒~分钟) | 高 | Selenium, Cypress |
| 集成测试 | 中(模块/接口) | 中(毫秒~秒) | 中 | Testcontainers, Spring Boot Test |
| 单元测试 | 细(函数/方法) | 快(微秒~毫秒) | 低 | JUnit, Jest, pytest |
嗯,这里要注意:金字塔并不是死规矩。如果你的项目是纯算法库,可能不需要端到端测试;如果是微服务系统,集成测试的比例会适当提高。但原则不变——单元测试永远是地基。
测试驱动开发(TDD)简介
TDD,全称Test-Driven Development。它不是一种测试技术,而是一种开发方法论。核心流程就是上面SVG里画的“红-绿-重构”循环:
- 红:先写一个会失败的测试(还没实现功能)
- 绿:写最简代码让这个测试通过
- 重构:优化代码,同时保证测试依然全绿
我在项目中实际用过TDD开发一个支付模块。起初觉得“先写测试再写代码”很反直觉,但坚持下来发现两个好处:一是代码设计更松耦合(因为你必须让类可测试),二是回归成本极低。我有多依赖它?有一次重构了整个核心算法,跑一遍测试只花了3秒,全部通过,直接上线——那种安全感,没试过TDD的人体会不到。
我的个人建议: 如果你刚开始学TDD,别一上来就要求100%覆盖率。先从几个关键业务函数练手,感受“红绿切换”的节奏。你会慢慢上瘾的。
来看一个真实TDD的例子:我们要写一个判断年份是否为闰年的函数。按照TDD步骤:
第一步(红): 先写测试,期望输入2000年返回true(闰年规则:能被4整除但不能被100整除,或者能被400整除)。
test('2000年是闰年', () => {
expect(isLeapYear(2000)).toBe(true);
});
test('1900年不是闰年', () => {
expect(isLeapYear(1900)).toBe(false);
});
此时函数还没写,测试报红。
第二步(绿): 写最简单实现。
function isLeapYear(year) {
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
return year % 4 === 0;
}
运行测试——全绿。
第三步(重构): 当前代码已经很清晰,无需改动。但你可以考虑将条件合并成一行,或者提取常量。记住:重构时不新增行为。
你可能会问:“TDD和单元测试是什么关系?” 很简单——TDD强迫你写单元测试,而且这些测试天然具有自文档、可回归的特性。我个人的经验是,即使不是严格使用TDD,至少做到“写功能前先写测试大纲”,能帮你节省大量调试时间。