当前位置: 首页 > 测试知识 > 阿里P8推荐的JUnit最佳实践:如何写出让人眼前一亮的单元测试
阿里P8推荐的JUnit最佳实践:如何写出让人眼前一亮的单元测试
2026-04-29 作者cwb 浏览次数97

写出让人眼前一亮的单元测试不是炫技,而是让测试成为活文档、安全网和设计反馈的合体。


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);


文章标签: 软件测试 测试工具
咨询软件测试