写出让人眼前一亮的单元测试不是炫技,而是让测试成为活文档、安全网和设计反馈的合体。
1. 测试命名当文档读,不当代码猜
方式:test1、testCalculate
用should_预期行为_When_触发条件命名,或用@DisplayName写出完整中文场景。
java
@Test
void should_ThrowInsufficientBalanceException_When_BalanceLessThanWithdrawAmount() { ... }
// 或用 @DisplayName,让测试报告直接生成需求文档
@DisplayName("提现金额超过余额时,应抛出余额不足异常并提示当前余额")
@Test void withdrawMoreThanBalance() { ... }
2. Given-When-Then用空白行和@Nested塑造结构
一个测试方法只测一个场景,但一个业务对象可以有多个场景。用@Nested类分层:
java
@DisplayName("银行账户取款")
class AccountWithdrawTest {
@Nested
@DisplayName("当账户状态正常时")
class WhenAccountActive {
@Test
@DisplayName("余额充足,取款成功")
void shouldSucceed() {
// Given
Account account = new Account(1000, true);
// When
account.withdraw(200);
// Then
assertThat(account.getBalance()).isEqualTo(800);
}
@Test
@DisplayName("余额不足,抛出异常")
void shouldThrowException() { ... }
}
@Nested
@DisplayName("当账户已冻结时")
class WhenAccountFrozen { ... }
}
IDE中展开折叠的@Nested就是一份可直接交付的业务规格文档。
3. 舍弃JUnit原生断言,投向AssertJ
assertEquals在多字段对比时像天书。用AssertJ的流式断言,让断言念出来像句子。
java
// 生硬的原生断言
assertEquals("John", user.getName());
assertEquals(30, user.getAge());
// 让人眼前一亮的 AssertJ
assertThat(user)
.extracting(User::getName, User::getAge)
.containsExactly("John", 30);
// 集合断言更惊艳
assertThat(orderList)
.filteredOn(order -> order.getAmount() > 100)
.hasSize(2)
.allMatch(o -> o.getStatus() == Status.PAID);
4. 参数化测试给边界值批量洗澡
不为每种预期输出重写一个@Test。借助@CsvSource、@MethodSource集中管理用例,修改一处即可。
java
@ParameterizedTest
@CsvSource({
"0, 0%",
"5000, 5%",
"10000, 10%",
"99999, 10%"
})
void should_CalculateCorrectTax(String income, String expectedRate) {
assertThat(taxService.calculate(income)).isEqualTo(expectedRate);
}
新增测试数据只需加一行,测试包括率从不完整到穷尽边界变得毫无成本。
5. 只Mock直接依赖,别Mock值对象和时间
大忌:Mock System.currentTimeMillis()、new Date()或List,会导致测试变得脆弱且掩盖设计问题。
正解:注入Clock依赖,用MockitoExtension精简Mock注入。
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock PaymentService paymentService;
@InjectMocks OrderService orderService;
@Test
void should_Fail_When_PaymentDeclined() {
given(paymentService.pay(any())).willReturn(false);
assertThrows(PaymentException.class, () -> orderService.place(order));
}
}
Mock的黄金法则:不要Mock你不拥有的类型。
6. 让异步测试比同步测试还清晰
Thread.sleep(1000)会拖慢所有测试并极不稳定。用Awaitility库配合assertTimeout,让等待变成非阻塞断言。
java
await().atMost(2, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(repo.findByStatus("DONE")).hasSize(1));
如果必须测超时用JUnit 5的assertTimeoutPreemptively,直接中断耗时线程。
7. 开启并行执行先学会写隔离的测试
在junit-platform.properties开启并行:
properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
每个测试方法独立创建上下文,不共享可变静态状态。如果有依赖关系,用@ResourceLock同步。会瞬间发现跑完上千个测试从几分钟变成十几秒。
8. 用@Tag制造CI流水线的快速卡点
java
@Tag("unit")
@Test void coreLogic() { }
@Tag("integration")
@SpringBootTest
class IntegrationTest { }
CI中Maven/Gradle命令 -Dgroups=unit 先快速执行单元测试(<30秒),通过后才启动全量集成测试速度翻倍。
9. 动态测试把维护从代码变回数据
当测试场景由外部规则驱动(如费率表、检查文件),用@TestFactory动态生成测试,无需每次新增@Test。
java
@TestFactory
Stream<DynamicTest> validateAllFeeScenarios() {
return feeRuleLoader.loadAll().stream()
.map(rule -> dynamicTest(rule.getScenario(), () ->
assertThat(calc(rule.getInput())).isEqualTo(rule.getExpect())
));
}
10. 测试代码也是产品代码用ArchUnit守护
让测试代码零烂代码,用架构测试保证规范:
java
@ArchTest
static final ArchRule no_junit4 = noClasses()
.should().dependOnClassesThat().resideInAPackage("org.junit.Test");
@ArchTest
static final ArchRule tests_must_be_display_named =
methods().that().areAnnotatedWith(Test.class)
.should().beAnnotatedWith(DisplayName.class);