JUnit4 基础回顾:注解、断言、测试套件、参数化测试、执行顺序控制

写 Android 回归测试,JUnit4 是躲不过的坎。我见过不少团队,上来就怼 Espresso 或 Robolectric,结果跑用例时连最基本的注解都写错——嗯,这种事我自己也干过。今天把这几个核心知识点捋清楚,后面集成 AndroidX Test 时才不会卡壳。

JUnit4 回归测试核心构件 JUnit4 测试框架 注解 断言 测试套件 参数化测试 执行顺序控制 常见注解 @Test, @Before, @After @BeforeClass, @AfterClass @Ignore, @Rule, @ClassRule 常见断言 assertEquals assertTrue / assertFalse assertNull / assertNotNull 测试套件 @RunWith(Suite.class) @Suite.SuiteClasses 批量执行 + 分类集合 参数化测试 @RunWith(Parameterized.class) @Parameters 数据驱动 执行顺序控制 @FixMethodOrder MethodSorters.NAME_ASCENDING 自定义 Order 注解 实践要点 AndroidX Test 环境下,注解和断言完全复用 JUnit4 机制

注解:测试的骨架

JUnit4 的注解体系,说白了就是帮框架识别「哪些方法是测试方法」「哪些是前置/后置操作」。我个人习惯把注解分成三类:

注解 作用域 执行时机
@Test 方法 标识一个测试用例,每个@Test都会独立运行
@Before 方法 每个@Test运行之前执行,做初始化(比如创建Activity)
@After 方法 每个@Test运行之后执行,做资源清理
@BeforeClass 静态方法 测试类中所有方法运行前执行一次(通常用于初始化数据库等重资源)
@AfterClass 静态方法 测试类所有方法运行后执行一次
@Ignore 方法/类 跳过该测试方法或整个测试类

关键区别:@BeforeClass/@AfterClass 必须是 public static void,且在整个类生命周期只跑一次。而 @Before/@After 每个测试方法都会执行一遍。写 Android 回归测试时,我通常把 Activity 准备放 @Before,把数据库关闭放 @AfterClass。

小技巧:如果某个测试方法需要临时跳过,直接用 @Ignore("原因") 比删代码更安全。我曾因为删了 @Test 忘了加回来,结果漏测了一个核心流程——嗯,后来再也不敢手欠了。

断言:测试的良心

断言就是「检查结果是不是我想要的」。JUnit4 提供了一系列静态断言方法,都在 org.junit.Assert 里。常用的就这几个:

  • assertEquals(expected, actual) —— 判断两个值相等(注意参数顺序,预期放前面)
  • assertNotEquals(unexpected, actual) —— 判断不相等
  • assertTrue(condition) / assertFalse(condition) —— 判布尔
  • assertNull(object) / assertNotNull(object) —— 判空
  • assertSame(expected, actual) / assertNotSame(expected, actual) —— 判断是否为同一引用
  • assertArrayEquals(expectedArray, actualArray) —— 数组内容逐一比较

我曾踩过的坑:用 assertSame 去比较两个值相等的字符串。JUnit 文档里写得很清楚:assertSame 检查的是引用地址,不是内容。字符串字面量虽然在JVM常量池里可能复用,但 new String("hello") 和 "hello" 就是不指向同一个对象。所以判断内容一定用 assertEquals。

测试套件:把用例组织起来

当你有几十上百个测试类,挨个点运行就太傻了。测试套件(TestSuite)能帮我们把多个测试类打包成一个「测试集合」,批量执行。

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
    LoginTest.class,
    HomePageTest.class,
    SettingsTest.class
})
public class AllRegressionTest {
    // 这个类不需要写任何代码
}

跑这个套件时,JUnit 会按 @Suite.SuiteClasses 里声明的顺序依次执行每个测试类中的所有 @Test 方法。注意,套件类本身不需要任何测试方法,它只是个容器。

实际项目中的用法:我会按模块分套件。比如 UserModuleTestSuite、PaymentModuleTestSuite,再建一个 FullRegressionSuite 把它们都包含进去。这样开发时只跑模块套件,上线前跑全量套件。

参数化测试:一份代码测多组数据

你想想看,如果一个方法需要验证多组输入输出,你难道要写十几个 @Test?参数化测试就是解决这个问题的。它用 @RunWith(Parameterized.class) 配合 @Parameters 注解。

@RunWith(Parameterized.class)
public class CalculatorParamTest {

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
            {1, 2, 3, true},
            {4, 5, 9, true},
            {0, 0, 1, false}   // 第三组:0+0≠1
        });
    }

    private int a, b, expected;
    private boolean success;

    public CalculatorParamTest(int a, int b, int expected, boolean success) {
        this.a = a;
        this.b = b;
        this.expected = expected;
        this.success = success;
    }

    @Test
    public void testAdd() {
        int result = Calculator.add(a, b);
        if (success) {
            assertEquals(expected, result);
        } else {
            assertNotEquals(expected, result);
        }
    }
}

参数化测试的好处是「数据驱动」:新增测试数据只需往 data() 里加一行,测试方法不需要改。我习惯把参数写在 CSV 文件里,测试类里再读进来——但那是进阶用法了,这里先不展开。

注意:参数化测试的构造函数参数顺序必须跟 data() 返回的 Object[] 顺序一致。我当初写反过,结果所有用例都断言失败,排查了半天才发现是参数顺序错了。

执行顺序控制:别让用例顺序坑了你

JUnit4 默认不保证测试方法的执行顺序。这其实是刻意设计的:每个测试应该是独立的,顺序不应该依赖。但实际回归测试中,有些场景确实需要控制顺序(比如先注册再登录再注销)。这时可以用 @FixMethodOrder。

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class OrderedTest {

    @Test
    public void testB_register() { /* 先注册 */ }
    
    @Test
    public void testA_login() { /* 虽然名字是A,但按NAME_ASCENDING会先跑 */ }
    
    @Test
    public void testC_logout() { /* 最后退出 */ }
}

MethodSorters 有三种模式:

  • NAME_ASCENDING —— 按测试方法名字典序升序执行
  • JVM —— 按JVM返回的顺序(不可控)
  • DEFAULT —— 确定的但不可预测的顺序(通常是文件名顺序)

我曾经踩的坑:在套件级别依赖执行顺序。比如一个套件里先跑 LoginTest 再跑 HomePageTest。但套件本身不保证顺序。所以要么用 @FixMethodOrder 在类级别控制,要么把依赖的多个步骤放到同一个 @Test 方法里。我个人更推荐后者——测试类之间尽量解耦,这才是单元测试的正确姿势。

好,JUnit4 的基础就这些。记住这几个核心要点,后面集成 AndroidX Test 框架时,你就能把精力放在 UI 测试逻辑上,而不是被这些基础配置绊住脚。