← 返回题目列表

JUnit 5 的生命周期、常用注解和参数化测试是什么?

高频 中等 第 7 / 23 题 更新于 2026/07/25
JUnit 5测试生命周期参数化测试

简化版

JUnit 5 由三部分组成Platform(运行测试的基础平台)、Jupiter(JUnit 5 的编程/扩展模型,@Test 等新注解来自这里)、Vintage(兼容运行 JUnit 3/4 老测试)。生命周期默认 per-method——每个测试方法都创建一个新的测试类实例,避免测试间共享状态。常用注解:@Test@BeforeEach/@AfterEach(每个方法前后)、@BeforeAll/@AfterAll(所有方法前后,默认要 static)参数化测试@ParameterizedTest + @ValueSource/@CsvSource/@MethodSource)用不同输入复用同一套断言逻辑,适合测边界值和多组规则。

详细版

JUnit 5 三大模块:

模块作用
Platform启动和运行测试的基础平台(IDE/构建工具对接它)
JupiterJUnit 5 的编程模型和扩展模型(@Test@ExtendWith 等)
Vintage兼容运行 JUnit 3/4 的旧测试

核心注解与生命周期:

class OrderServiceTest {
    @BeforeAll   // 所有测试前 1 次(默认必须 static)
    static void initExpensiveResource() { }

    @BeforeEach  // 每个测试方法前
    void setUp() { /* 准备每个测试都要的新对象 */ }

    @Test
    void shouldCalculatePrice() { /* 断言业务结果 */ }

    @AfterEach   // 每个测试方法后
    void tearDown() { /* 清理临时状态 */ }

    @AfterAll    // 所有测试后 1 次(默认 static)
    static void cleanup() { }
}

参数化测试(数据驱动):

@ParameterizedTest
@CsvSource({ "1, 1, 2", "2, 3, 5", "-1, 1, 0" })
void shouldAdd(int a, int b, int expected) {
    assertEquals(expected, calculator.add(a, b));   // 同一套断言,跑多组数据
}

@ParameterizedTest
@ValueSource(strings = { "", " ", "  " })
void blankStringsAreInvalid(String s) {
    assertFalse(validator.isValid(s));
}

常用输入源@ValueSource(单参数简单值)、@CsvSource(少量表格数据)、@MethodSource(复杂对象/动态生成)、@EnumSource@NullAndEmptySource

完整版教学

一、JUnit 5 的组成(三大模块)

JUnit 5 不是一个单体,而是三个模块:

  • JUnit Platform运行测试的基础平台。IDE(IDEA)、构建工具(Maven Surefire、Gradle)都通过它来发现和执行测试。它定义了 TestEngine 接口。
  • JUnit JupiterJUnit 5 自己的编程模型和扩展模型。你日常写的 @Test@ParameterizedTest@BeforeEach@ExtendWith大多来自 Jupiter
  • JUnit Vintage兼容层,让你能在 JUnit 5 平台上继续运行 JUnit 3/4 写的老测试,方便渐进迁移。

面试能说清「Platform 是平台、Jupiter 是新模型、Vintage 是兼容旧版」就到位了。

模块面向谁典型作用
PlatformIDE/构建工具/测试引擎发现、启动、执行测试
Jupiter测试编写者提供 JUnit 5 注解和扩展模型
Vintage迁移中的老项目在 JUnit 5 平台运行 JUnit 3/4 测试

JUnit 5 面试别只背注解,要先说清 Platform/Jupiter/Vintage 的分层,再谈生命周期和参数化测试。

二、默认生命周期为什么重要(per-method)

JUnit 5 默认生命周期是 per-method(PER_METHOD每个测试方法执行前,都会创建一个新的测试类实例

为什么这样设计?为了测试隔离——减少测试之间的状态污染。如果所有测试方法共用一个实例,一个测试改了实例字段,会影响后面的测试,导致测试互相干扰、结果依赖执行顺序(极难排查的问题)。每个方法新实例,就保证了实例字段不会在测试之间共享

但注意:这只隔离了实例字段。如果你把可变状态放在 static 字段外部资源(数据库、文件)里,测试之间仍然可能互相影响。所以测试独立性不能只靠框架保证——你自己也要避免共享可变状态。

(可以用 @TestInstance(Lifecycle.PER_CLASS) 改成整个类共用一个实例,此时 @BeforeAll/@AfterAll 可以非 static,但要自己管好状态清理。)

PER_METHOD: testA -> new TestClass(), testB -> new TestClass()
PER_CLASS : testA/testB 共用同一个 TestClass 实例

三、Before / After 的执行边界

四个生命周期注解的边界:

  • @BeforeEach每个测试方法前执行。适合准备每个测试都需要的、新的对象(如 new 一个被测对象、重置 mock),保证每个测试从干净状态开始。
  • @AfterEach每个测试方法后执行。适合清理临时状态(关资源、清 ThreadLocal)。
  • @BeforeAll所有测试方法之前只执行 1 次。适合昂贵且可共享的准备工作(启动 Testcontainers 容器、加载大配置)。默认要求是 static 方法(因为此时还没有实例)。
  • @AfterAll所有测试方法之后只执行 1 次,清理共享资源。

注意@BeforeAll 里的共享资源要格外小心并发和清理——多个测试共用一个资源,如果测试有并发或资源有状态,可能互相影响。

四、断言要表达业务结果

断言不是只检查「不报错」。写测试时断言要表达真正的业务预期

  • 测金额计算,要断言金额值、舍入方式、异常分支、边界值,而不是「跑通了就行」。
  • assertEqualsassertTrueassertThrows 等明确断言预期。
  • assertAll 可以把多个相关断言组合起来一起执行——这样第一个断言失败不会遮住后面的(普通断言第一个失败就停,看不到其他问题):
assertAll("order",
    () -> assertEquals(100, order.getAmount()),
    () -> assertEquals("PAID", order.getStatus()),
    () -> assertNotNull(order.getPaidTime())
);

没有断言、或断言太弱的测试没有保护价值——它只是让覆盖率好看,实际测不出 bug。

五、参数化测试的价值

同一段逻辑需要用多组输入验证时(边界值、等价类、多组业务规则),参数化测试把「测试结构」和「测试数据」分开,比复制十个类似的测试方法好维护得多

  • @ValueSource:单个参数的简单值列表(字符串、数字)。
  • @CsvSource少量表格数据,每行一组输入(含期望值)——适合「输入→期望输出」的规则表。
  • @MethodSource:从一个方法返回复杂对象或动态生成的数据,适合参数复杂的情况。
  • @NullAndEmptySource:自动测 null 和空值(常见边界)。

一套断言逻辑 + 多组数据,既减少重复代码,又让「测了哪些 case」一目了然。

六、异常测试怎么写

assertThrows 验证异常,但要验得具体

@Test
void shouldRejectNegativeAmount() {
    IllegalArgumentException ex = assertThrows(
        IllegalArgumentException.class,          // 验证具体异常类型
        () -> account.withdraw(-100)
    );
    assertTrue(ex.getMessage().contains("金额"));  // 验证异常消息/业务字段
    assertEquals(1000, account.getBalance());     // 验证失败后状态没被错误修改
}
  • 只断言「抛了 RuntimeException」太粗——会把完全不同的错误混在一起(可能是 NPE 而非业务异常)。要断言具体的异常类型
  • 还要验证异常后的状态——失败后账户余额有没有被错误扣减(异常不能留下脏数据)。

七、和 Spring 测试的边界(别滥用 @SpringBootTest)

纯单元测试不需要启动 Spring 容器——直接 new 被测对象、用 Mockito 替换依赖,更快更清晰。只有当你要验证框架行为时才用 Spring Test:

  • Bean 装配、依赖注入是否正确@SpringBootTest 或切片测试。
  • 事务、配置绑定 → Spring Test。
  • MVC 请求@WebMvcTest + MockMvc。

反模式:把所有测试都写成 @SpringBootTest——每个测试都启动完整 Spring 上下文,慢到没人愿意跑,失去了单元测试「快速反馈」的意义。原则:能 new 就 new,非要框架才上 Spring Test,且优先用切片测试(@WebMvcTest 等)而非全量 @SpringBootTest

八、常见误区与追问

  • 误区:JUnit 5 只是 JUnit 4 换了一批注解。 JUnit 5 分为 Platform、Jupiter、Vintage,运行平台和编程模型都重新拆分了。
  • 误区:per-method 能隔离所有状态。 它只隔离测试实例字段,static 字段、数据库、文件等外部状态仍要清理。
  • 误区:参数化测试只是为了少写几个测试方法。 它的价值是把输入数据和断言逻辑分开,更适合系统覆盖边界值和等价类。
  • 追问:@BeforeAll 默认为什么要 static? 默认 per-method 下执行 @BeforeAll 时还没有具体测试实例,所以要求静态方法。
  • 追问:什么时候使用 @MethodSource 当参数是复杂对象、动态生成数据或多字段组合超出简单 CSV 表达能力时使用。
  • 追问:为什么不建议所有测试都用 @SpringBootTest 它启动完整 Spring 上下文,反馈慢,容易把单元测试变成沉重的集成测试。

九、加强记忆

JUnit 5 = Platform(平台)+ Jupiter(新编程模型,@Test 来源)+ Vintage(兼容 JUnit 3/4)。生命周期默认 per-method(每个测试方法新建实例)——隔离实例字段避免污染,但 static 字段/外部资源仍会互相影响(独立性别只靠框架)。注解:@BeforeEach/@AfterEach(每方法前后,准备/清理)、@BeforeAll/@AfterAll(所有方法前后 1 次,默认 static,放昂贵共享资源)断言要表达业务结果(值/舍入/异常/边界,assertAll 组合避免遮蔽、assertThrows 验具体异常类型+失败后状态)。参数化测试@ParameterizedTest + @ValueSource/@CsvSource/@MethodSource数据驱动、一套断言跑多组数据别滥用 @SpringBootTest(能 new 就 new,慢反馈害死单测)。