工具类单元测试需覆盖语义全场景:正常输入、边界值、非法参数、空值、溢出及底层异常;必须显式断言所有throw路径;抽离静态依赖提升可测性;以JaCoCo分支覆盖率≥90%为质量基准。

工具类(Utility Class)通常由静态方法组成,无状态、无依赖,看似简单,但恰恰容易被测试忽略边界和异常路径,导致覆盖率虚高、质量不足。要写出高覆盖率的单元测试,关键不是“多写几个@Test”,而是覆盖语义全场景:正常输入、边界值、非法参数、空值、溢出、以及可能触发的底层异常。
覆盖所有输入类型与边界条件
工具类方法常对入参有隐式契约(比如“非 null”、“> 0”、“长度 ≤ 100”)。只测一个合法值远远不够。
- 对数值型参数:测试最小值、最大值、0、负数、Integer.MIN_VALUE/MAX_VALUE
- 对字符串:测试 null、空串("")、空白串(" ")、单字符、超长字符串、含特殊字符(如换行、控制符)
- 对集合/数组:测试 null、空集合、单元素、多元素、含 null 元素
例如,一个字符串截取工具:
public static String truncate(String s, int len) {if (s == null) return null;
if (len < 0) throw new IllegalArgumentException("length must be >= 0");
return s.substring(0, Math.min(len, s.length()));
}
对应测试需覆盖:truncate(null, 5)、truncate("", 3)、truncate("abc", -1)、truncate("hello", 10)、truncate("x", 0) —— 这些组合才能真正打穿分支逻辑。
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
强制触发并断言所有异常路径
工具类中抛出的 IllegalArgumentException、NullPointerException、IndexOutOfBoundsException 等,不是缺陷,而是设计契约。JUnit 5 要求用 assertThrows 显式验证,否则这些 throw 行在覆盖率报告中会被标记为“未执行”。
- 每个
throw语句必须有至少一个测试用例精准命中 - 避免用
@Test(expected = ...)(JUnit 4 风格),它不校验异常消息,也不支持后续断言 - 推荐写法:
assertThrows(IllegalArgumentException.class, () -> truncate("a", -5)).getMessage().contains("length")
处理静态方法的可测性陷阱
工具类没有实例状态,但可能间接依赖静态资源(如 System.currentTimeMillis()、LocalDateTime.now()、UUID.randomUUID()),这类调用会让测试不可重复、难 Mock。
- 重构建议:将易变依赖抽成函数式接口参数(如
Supplier<Long> clock),测试时传入固定值 - 若无法重构,可用
Mockito.mockStatic()(需 Mockito 3.4.0+)临时拦截,但仅限必要场景 - 绝对避免在工具方法里直接读配置文件、连数据库或发 HTTP 请求——这已违反工具类定义,应移至服务层
用 JaCoCo 验证真实覆盖率,不止看“行覆盖”
运行 mvn test jacoco:report 后,打开 target/site/jacoco/index.html,重点关注两个指标:
- 分支覆盖率(Branch Coverage):if/else、三元运算、switch 的每个分支是否都走到了?工具类中多层 if 判断很常见,仅行覆盖 100% 不代表逻辑全覆盖
-
行覆盖率(Line Coverage):确认
throw行、return null、else { }空块是否被点亮
若分支覆盖率低于 90%,说明至少有一个 if 分支没被测试到——回头检查是否漏了负数、null 或边界值用例。

















