3. JUnit基础回顾:从4到5,测试的骨架

聊Compose UI测试之前,咱得先把地基打牢。JUnit就是那个地基。不管你用JUnit 4还是JUnit 5,核心思想是一样的:写断言、管生命周期、做参数化。我个人习惯在项目里直接上JUnit 5,毕竟新项目没必要抱着老版本不放。但为了照顾还在维护老项目的朋友,我会把两套API都点到。

一句话总结:JUnit提供了一套"什么时候跑、怎么跑、跑完怎么办"的规约,Compose测试框架就站在这个肩膀上。

3.1 注解:标记测试的"贴纸"

注解是JUnit的身份证。你用哪个版本,贴纸就不一样。我见过不少新手把JUnit 4的注解混到JUnit 5里,结果测试跑不起来——嗯,踩坑踩出来的经验。

功能 JUnit 4 JUnit 5
标识测试方法 @Test @Test
每个测试前执行 @Before @BeforeEach
每个测试后执行 @After @AfterEach
所有测试前执行(一次) @BeforeClass @BeforeAll
所有测试后执行(一次) @AfterClass @AfterAll
禁用测试 @Ignore @Disabled

看到了吧?JUnit 5把名字改得更语义化。"BeforeEach"比"Before"清楚多了——每个测试方法之前执行。我在一个老项目里看到有人用@Before做全局初始化,结果每次跑测试都开数据库连接,那叫一个慢……后来换成@BeforeAll,一秒搞定。

我曾经在JUnit 5项目里顺手写了@Before,编译器没报错,但方法压根没执行。排查半天才发现导错了包。记住:@Before是junit 4的,@BeforeEach是junit 5的。混用必挂。

3.2 断言:测试的"测谎仪"

断言就是你的判断依据。你设想程序应该返回什么,断言帮你验证。JUnit 5在这方面做了大升级——引入了Lambda风格的断言,失败信息更友好。

核心断言方法

  • assertEquals(expected, actual) —— 最常见,比相等。
  • assertNotEquals(unexpected, actual) —— 反过来。
  • assertTrue(condition) / assertFalse(condition) —— 检查布尔值。
  • assertNull(object) / assertNotNull(object) —— 检查空不空。
  • assertThrows(exceptionClass, executable) —— 验证异常,JUnit 5新增。
  • assertAll(executables...) —— 合并多个断言,如果一个失败,不会终止后续断言。

举个例子:

@Test
void testCalculate() {
    int result = calculator.add(2, 3);
    assertEquals(5, result, "加法结果应该为5");
    
    // 多个断言一起跑
    assertAll("多项检查",
        () -> assertEquals(5, result),
        () -> assertTrue(result > 0)
    );
}

你想想看,那个assertAll多有用。假如你有五个断言,第一个就挂了,后面四个压根不会执行。等你修好第一个,又得跑一遍才能看到第二个报错。用了assertAll,一次全报出来,省时间。我在做Compose UI测试时经常用它——检查多个控件状态一次性搞定。

assertThrows 是JUnit 5的亮点。以前在JUnit 4里想测异常,得用@Test(expected = ...),那玩意儿不能验证异常消息,也不够灵活。现在可以这样写:
Exception ex = assertThrows(IllegalArgumentException.class, 
    () -> new User(null));
assertEquals("Name must not be null", ex.getMessage());

3.3 测试生命周期:什么时候做什么事

说白了,测试生命周期就是告诉你"初始化、跑测试、清理"这三步怎么安排。JUnit 5用@BeforeAll、@BeforeEach、@AfterEach、@AfterAll四个注解搞定。

一张图看明白执行顺序:

JUnit 5 测试生命周期 @BeforeAll @BeforeEach @Test @AfterEach N个@Test重复执行 @AfterAll 注意 BeforeAll/AfterAll 必须是 static (JUnit 5中可改为 interface default)

注意看,@BeforeEach和@AfterEach会在每个@Test前后各跑一次。假如你有10个测试方法,它们就各跑10次。而@BeforeAll和@AfterAll只跑一次——当然,你得把它们声明成static方法。我记得刚学的时候忘了加static,IDE也没报错,结果测试跑起来奇慢——每个测试类实例化都重复初始化,嗯,后来再没犯过。

JUnit 5允许你不把@BeforeAll/@AfterAll写成static,前提是测试类实现了TestInstance.Lifecycle.PER_CLASS接口。这样整个测试类只创建一个实例,适合需要共享状态但不想用static的场景。但说实话——我建议少用,容易引入测试间依赖。

3.4 参数化测试:一套数据,多次验证

你有没有遇到过这种场景:一个加法方法,你想验(1,2,3)、(0,0,0)、(-1,1,0)等好几组数据?以前在JUnit 4里得手动写多个@Test,或者用@Parameterized配合@RunWith(Parameterized.class),那配置量——啧,烦。

JUnit 5把这个简化了。核心就两步:① 给测试类加@ParameterizedTest ② 用@ValueSource、@CsvSource、@MethodSource等提供数据源。

常用数据源注解

注解 适用场景
@ValueSource 单参数简单值(int、String等)
@CsvSource 多参数,用逗号分隔,一行一组数据
@MethodSource 复杂类型或动态生成数据,返回Stream或数组
@EnumSource 枚举类型遍历

来看个实际例子:

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "0, 0, 0",
    "-1, 1, 0",
    "100, 200, 300"
})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calculator.add(a, b));
}

你看,四组数据用一个测试方法搞定。我以前给一个支付模块写测试,需要验证不同金额、不同币种的组合,用@CsvSource一口气列了二十多组,代码整洁多了。

我曾经在@CsvSource里漏了空行导致的bug。注意:如果某组数据里有空字符串,要写成''(两个单引号)。如果数据本身包含逗号,需要用双引号括起来,比如"hello, world", 1。不然解析会崩。

另外,@MethodSource能处理更复杂的场景。比如参数里有对象:

@ParameterizedTest
@MethodSource("userProvider")
void testUserValidation(User user, boolean isValid) {
    assertEquals(isValid, validator.validate(user));
}

static Stream<Arguments> userProvider() {
    return Stream.of(
        Arguments.of(new User("Alice", 25), true),
        Arguments.of(new User("", 25), false),
        Arguments.of(new User("Bob", -1), false)
    );
}

这个方法名要跟@MethodSource里写的一致,而且必须返回Stream<Arguments>或Arguments[]。我习惯把数据提供方法统一放在测试类底部,用static修饰——干净。

3.5 常用断言速查表

为了让你随手翻看方便,我整理了一个小表格。贴在工位上,写测试时瞄一眼。

断言方法 说明 JUnit版本
assertEquals(a, b) 检查a等于b(会调用equals) 4/5
assertNotEquals(a, b) 检查a不等于b 4/5
assertSame(a, b) 检查a和b是同一个引用(==) 4/5
assertNotSame(a, b) 检查a和b不是同一个引用 4/5
assertNull(a) 检查a为null 4/5
assertNotNull(a) 检查a不为null 4/5
assertTrue(condition) 检查条件为true 4/5
assertFalse(condition) 检查条件为false 4/5
assertThrows(type, exe) 检查执行的异常类型 5
assertAll(exe...) 合并多个断言,失败不中断 5
assertArrayEquals(a, b) 检查数组内容相等 4/5

你可能会问:assertEquals和assertSame有啥区别?一个比内容,一个比引用。我在一次面试题里见过这个坑——用assertEquals检查两个Integer对象,因为Integer缓存池的影响,-128到127之间是同一引用,超出范围就不是了。如果你真想比引用,用assertSame。但99%的场景下,用assertEquals就够了。

3.6 小结

写到这,JUnit基础回顾就差不多了。你只要记住:注解管"时机",断言管"对错",生命周期管"顺序",参数化管"数据"。Compose UI测试会大量用到这些——比如用@BeforeEach初始化Compose测试规则,用@ParameterizedTest验证不同UI状态。打好这个底子,后面讲Compose UI测试框架时才不会发懵。