边界与单元测试:使用第三方代码、学习性测试、整洁的测试、F.I.R.S.T原则
说实话,很多团队在引入第三方库的时候,都踩过同一个坑——以为文档写得好,代码就能无缝对接。我当年刚带项目时也这么想,结果被一个日志框架的版本兼容问题折腾了整整两天。后来我悟了:边界,才是最容易出问题的地方。
今天咱们就聊聊,怎么用单元测试把这层边界管好。
1. 第三方代码:别信文档,信测试
第三方库,说白了就是别人写的代码。你没法控制它的内部逻辑,也没法保证它永远按你的预期跑。我见过太多项目,升级一个依赖库,结果整个模块崩了——因为没人知道那个库到底干了什么。
那怎么办?用测试把边界封死。
举个例子,假设你的项目里用了某个 JSON 解析库。你可能会这样写:
// 不推荐:直接调用第三方 API,没有隔离
public class UserService {
public User parseUser(String json) {
return SomeJsonLib.parse(json, User.class);
}
}
这样写的问题在于:你的核心业务逻辑和第三方库绑死了。一旦库换了接口,或者行为变了,你整个类都得改。
我个人的习惯是:在第三方库外面包一层自己的接口。就像这样:
// 推荐:定义自己的接口
public interface JsonParser {
<T> T parse(String json, Class<T> clazz);
}
// 实现类:封装第三方库
public class SomeJsonLibParser implements JsonParser {
@Override
public <T> T parse(String json, Class<T> clazz) {
return SomeJsonLib.parse(json, clazz);
}
}
这样一来,你的业务代码只依赖 JsonParser 接口。换库?改一个实现类就行。测试?直接 mock 接口,不用碰第三方库。
2. 学习性测试:用代码验证文档
你有没有这种经历?看文档觉得懂了,一写代码就报错。我以前也这样,直到我开始做学习性测试。
说白了,就是针对第三方库写测试,验证它的行为是否符合你的预期。这不是为了测库本身,而是为了让你自己放心。
举个例子,假设你要用 Guava 的 Cache:
@Test
public void testGuavaCacheExpiration() {
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
cache.put("key", "value");
Assert.assertEquals("value", cache.getIfPresent("key"));
// 等 1.5 秒
Thread.sleep(1500);
Assert.assertNull(cache.getIfPresent("key"));
}
这个测试跑通了,你才敢放心用。否则,你永远不知道文档里写的「1秒过期」到底是不是真的。
3. 整洁的测试:可读性第一
我见过最糟糕的测试,一个方法里塞了 200 行,又是 mock 又是断言,最后连写测试的人自己都看不懂。嗯,这种测试,说白了就是负债。
整洁的测试,应该像一篇好文章:读起来顺畅,逻辑清晰。我总结了几个要点:
- 一个测试只测一件事。别想着一个方法测三个场景,拆开写。
- 用有意义的命名。比如
shouldReturnNullWhenCacheExpired,比testCache1好一百倍。 - 遵循 Arrange-Act-Assert 模式。准备数据、执行操作、验证结果,三段式清晰明了。
看个例子:
@Test
public void shouldReturnNullWhenCacheExpired() {
// Arrange
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
cache.put("key", "value");
// Act
Thread.sleep(1500);
String result = cache.getIfPresent("key");
// Assert
Assert.assertNull(result);
}
你看,这样写,任何人一看就知道这个测试在干什么。
4. F.I.R.S.T 原则:好测试的五个标准
F.I.R.S.T 是五个单词的首字母缩写,代表了整洁测试的五个核心原则。我把它整理成了表格,方便你对照:
| 字母 | 含义 | 说明 | 我的经验 |
|---|---|---|---|
| F | Fast(快速) | 测试应该跑得飞快,几秒内完成 | 我曾经有个测试跑 5 分钟,后来发现是连了数据库 |
| I | Independent(独立) | 测试之间互不依赖,可以单独运行 | 别让测试 A 依赖测试 B 的结果,那是灾难 |
| R | Repeatable(可重复) | 任何环境都能跑出相同结果 | 别依赖系统时间、随机数、网络状态 |
| S | Self-Validating(自验证) | 测试结果只有通过/失败,不需要人工判断 | 别写那种「跑完看日志」的测试 |
| T | Timely(及时) | 测试应该在写生产代码之前或同时写 | TDD 就是最好的实践 |
这五个原则,说白了就是一句话:测试要快、要稳、要准。你想想看,如果每次跑测试都要等 10 分钟,谁还愿意跑?如果测试结果不稳定,今天过明天不过,那它还有什么意义?
5. 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
这张图展示了本章的四个核心模块。你可以看到,第三方代码和学习性测试解决的是「怎么用别人的代码」的问题,整洁的测试和 F.I.R.S.T 原则解决的是「怎么写出好测试」的问题。两者结合,才能把边界管好。
好了,边界与单元测试的核心内容就这些。记住一句话:边界是风险,测试是防线。别等到线上出问题了,才后悔没写测试。