边界与单元测试:使用第三方代码、学习性测试、整洁的测试、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);
}

你看,这样写,任何人一看就知道这个测试在干什么。

注意: 别在测试里写 if-else 或循环。测试应该是线性的,分支逻辑会让人困惑。

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 分钟,谁还愿意跑?如果测试结果不稳定,今天过明天不过,那它还有什么意义?

避坑指南: 我曾经写过依赖数据库的测试,结果 CI 环境连不上库,测试全挂。从那以后,我所有测试都只用内存数据。

5. 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

边界与单元测试知识体系 边界与单元测试 第三方代码 封装接口,隔离依赖 学习性测试 用代码验证文档 整洁的测试 可读性第一 F.I.R.S.T 原则 快、独立、可重复、自验证、及时 核心目标:让边界代码可测试、可维护、可信任

这张图展示了本章的四个核心模块。你可以看到,第三方代码和学习性测试解决的是「怎么用别人的代码」的问题,整洁的测试和 F.I.R.S.T 原则解决的是「怎么写出好测试」的问题。两者结合,才能把边界管好。


好了,边界与单元测试的核心内容就这些。记住一句话:边界是风险,测试是防线。别等到线上出问题了,才后悔没写测试。