刚入行那会儿,我写的单元测试总是被组长打回来重改。不是覆盖率不够,就是逻辑混乱得像一团毛线。后来才明白,写测试不是随便调几个方法、比对个结果就完事了,它也有自己的规矩。
命名清晰,一眼看懂测什么
函数名是沟通的第一步。别用 test1、checkSomething 这种名字糊弄自己。好的命名应该说明“在什么情况下,执行什么操作,预期什么结果”。比如:
shouldReturnFalseWhenUserIsNotActive()
throwsExceptionIfAmountIsNegative()
returnsEmptyListWhenNoOrdersExist()
这样的名字读一遍就知道这段测试想验证啥,出问题也容易定位。
一个测试只做一件事
有人喜欢在一个测试里塞一堆断言,觉得省事。但这样一旦失败,你就得花时间排查到底是哪一环出了问题。更合理的做法是:每个测试专注验证一个行为。
比如用户登录逻辑,不要写一个大而全的测试,而是拆成:
- 密码错误时拒绝登录
- 账户锁定后无法登录
- 正确凭证能成功进入系统
每个点独立成测试,互不干扰。
善用 setup 和 teardown
重复代码是维护的噩梦。如果每个测试开头都要创建用户、初始化配置,不如把这些共性操作提到 setUp() 里统一处理。
@BeforeEach
void setUp() {
database = new InMemoryDatabase();
userService = new UserService(database);
}
这样主测试逻辑就能更干净,聚焦在核心场景上。
避免测试依赖外部环境
网络请求、数据库连接、文件读写这些外部依赖会让测试变得不稳定。今天通过了,明天可能因为服务没启动就报错。这时候就得靠“模拟”来隔离。
比如你要测订单是否计入统计,但不想真连数据库,可以用 mock 模拟返回值:
OrderService orderService = mock(OrderService.class);
when(orderService.getTotalCount()).thenReturn(5);
// 接着去测你的统计逻辑
这样一来,测试跑得快,也不怕环境波动。
断言要具体,别图省事
写断言时,很多人习惯用 assertTrue(result) 就完事。但如果失败了,你根本不知道实际值是多少。更好的方式是指明期望:
assertEquals("expected 3 items", 3, items.size());
assertNotNull("user should not be null", user);
加上描述信息后,失败时输出的日志会更友好,查问题也更快。
测试也要保持整洁
有些人觉得测试代码不用太讲究,反正不跑在生产环境。可实际上,测试代码的维护成本常常超过业务代码。格式混乱、变量命名随意、逻辑嵌套过深,都会让后续修改举步维艰。
把它当成正式代码来写:提取公共方法、删除冗余注释、保持缩进一致。你会发现,整洁的测试反而能帮你更好理解业务逻辑。
覆盖关键路径,而不是追求数字游戏
团队定个“测试覆盖率必须达到80%”的目标很常见,但别为了数字堆砌无意义的测试。重点应该是核心流程、边界条件和异常分支。
比如处理金额的方法,除了正常计算,更要关注负数、零值、超大数值这些边缘情况。这些才是最容易出 bug 的地方。