单元测试与工程质量:可测试设计、断言/参数化/Mock、基准与覆盖
大约 4 分钟
单元测试与工程质量:可测试设计、断言/参数化/Mock、基准与覆盖
新手一屏速览
- 先设计可测试:可替换依赖(接口/构造注入)、可控时钟与随机种子
- 用参数化覆盖边界;Mock 只在外部依赖边界;业务逻辑避免过度 Mock
- 建立基准(JMH)与性能回归;稳定性优先,减少“雪花测试”
1. 可测试设计(Design for Testability)
- 依赖注入:构造器注入优先;对时间/随机引入
Clock/Random抽象 - 纯函数优先;最小副作用;将 IO/网络压到边界层
2. 测试类型与金字塔
- 单元测试(多且快)、集成测试(少而稳)、端到端(更少)
- 单元测试隔离业务,集成测试覆盖关键链路,端到端验证协议与回归
3. 断言、参数化与数据驱动
- 参数化用例覆盖边界与等价类;表驱动测试简化冗余用例
- 对浮点/时间/集合保持容忍度与稳定排序断言
4. Mock/Stubs/Fakes 与隔离
- Mock 外部系统与难控依赖(网络/数据库);Fake 轻量替身;Stub 固定响应
- 只在边界层 Mock;核心业务尽量纯逻辑测试,降低脆弱性
5. 覆盖率、变异测试与质量门禁
最近建一些几十个工作内推群,各大城市都有,群里目前已经收集了很多内推岗位,大厂、中厂、小厂、外包都有。 欢迎HR、开发、测试、运维和产品加入。

扫描下方微信,备注:网站+所在城市,即可拉你进工作内推群。

- 覆盖率是手段非目标;关注条件分支与边界;引入变异测试评估用例有效性
- 将测试与质量门禁集成到 CI;对不稳定用例设置隔离与重试策略
6. 基准与性能回归(JMH)
- 为关键算法/集合操作/序列化等建立基准;基于阈值告警性能回退
- 环境与 JVM 参数固定;结果以趋势分析为主,避免单点波动误判
7. 实战清单与反模式
- 清单:构造注入;参数化与边界覆盖;Mock 在边界;引入变异测试;建立基准与告警
- 反模式:雪花测试;过度 Mock;仅追求覆盖率数字;在 CI 中放行不稳定用例
8. 练习
- 为一个价格计算器设计可测试接口与参数化用例;对时间相关逻辑注入
Clock - 为集合热点方法建立 JMH 基准,并配置阈值化告警
示例代码(可直接复制运行)
示例一:可测试设计(注入 Clock)
import java.time.*;
// 业务:在 2024-11-11 当天 0 点到 23:59:59 下单折扣 20%
class DiscountService {
private final Clock clock;
DiscountService(Clock clock) { this.clock = clock; }
double price(double original) {
LocalDate today = LocalDate.now(clock);
boolean isPromo = today.getMonthValue()==11 && today.getDayOfMonth()==11;
return isPromo ? original * 0.8 : original;
}
}
public class DiscountDemo {
public static void main(String[] args) {
// 测试:注入固定时间保证稳定性
Clock promo = Clock.fixed(LocalDateTime.of(2024,11,11,12,0).toInstant(ZoneOffset.UTC), ZoneOffset.UTC);
Clock normal = Clock.fixed(LocalDateTime.of(2024,11,12,12,0).toInstant(ZoneOffset.UTC), ZoneOffset.UTC);
DiscountService s1 = new DiscountService(promo);
DiscountService s2 = new DiscountService(normal);
assert Math.abs(s1.price(100) - 80) < 1e-9;
assert Math.abs(s2.price(100) - 100) < 1e-9;
System.out.println("ok");
}
}示例二:表驱动(参数化)用例
import java.util.*;
class Tax {
static double apply(double base) {
if (base <= 100) return base * 1.0;
if (base <= 1000) return base * 1.1;
return base * 1.2;
}
}
public class TableDrivenTest {
record Case(double in, double out) {}
public static void main(String[] args) {
List<Case> cases = List.of(
new Case(50, 50),
new Case(100, 100),
new Case(500, 550),
new Case(2000, 2400)
);
for (Case c : cases) {
double got = Tax.apply(c.in);
if (Math.abs(got - c.out) > 1e-9) throw new AssertionError(c + " -> " + got);
}
System.out.println("all passed");
}
}示例三:边界条件与异常断言
public class ExceptionTest {
static int parsePositive(String s){
int v = Integer.parseInt(s);
if (v < 0) throw new IllegalArgumentException("negative");
return v;
}
public static void main(String[] args) {
try { parsePositive("-1"); throw new AssertionError("should fail"); }
catch (IllegalArgumentException expected) { System.out.println("ok"); }
}
}示例四:极简性能基准(注意仅演示)
public class MiniBench {
static long fib(int n){ return n<=1? n : fib(n-1)+fib(n-2); }
public static void main(String[] args) {
long t0 = System.nanoTime();
long s = 0;
for (int i=0;i<10;i++) s += fib(30);
long dt = System.nanoTime() - t0;
System.out.println("sum="+s+" time(ms)="+dt/1_000_000.0);
}
}