JUnit4 基础回顾:注解、断言、测试套件、参数化测试、执行顺序控制
写 Android 回归测试,JUnit4 是躲不过的坎。我见过不少团队,上来就怼 Espresso 或 Robolectric,结果跑用例时连最基本的注解都写错——嗯,这种事我自己也干过。今天把这几个核心知识点捋清楚,后面集成 AndroidX Test 时才不会卡壳。
注解:测试的骨架
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 测试逻辑上,而不是被这些基础配置绊住脚。