第3章 JUnit4基础:@Test注解、断言、生命周期

我们在AndroidX Test中写自动化回归用例,JUnit4是绕不开的基石。我个人习惯把JUnit4比作测试的“语法框架”——它规定了你该怎么声明一个测试、怎么验证结果、怎么安排前后置操作。这三块搞明白了,后面写Instrumentation测试、Espresso测试都会顺手得多。

这一章,我们只聊三个核心概念:@Test注解、断言、生命周期。别觉得简单,我面试过不少自称“精通JUnit”的工程师,问深了照样翻车。好,开始吧。

3.1 @Test注解——测试方法的“身份证”

在JUnit4里,一个普通方法要变成测试方法,只需要在它上面加一个@Test注解。编译器、运行器看到这个标记,就会把它当作测试用例来执行。

基本规则:

  • 方法必须是public的
  • 返回值必须是void
  • 不能有参数(除非使用参数化测试扩展)
@Test
public void testLoginSuccess() {
    // 测试代码
    assertTrue(true);
}

你可能觉得这太基础了,但我真的见过有人把@Test加在private方法上,然后死活跑不起来。嗯,这里要注意,JUnit4是通过反射调用测试方法的,private方法拿不到,会报java.lang.Exception: Method xxx should be public。

@Test还可以带两个可选属性:expected和timeout。

  • expected:指定期望抛出的异常,如果测试方法执行中抛出了该异常,则测试通过。
  • timeout:指定超时时间(毫秒),如果测试方法执行超过这个时间,则测试失败。
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
    new User("", -1);
}

@Test(timeout = 1000)
public void testNetworkCall() {
    // 如果超过1秒,测试自动失败
    fetchData();
}

我的习惯:timeout适合稳定性回归用例,防止某次网络抖动导致整个用例集卡死。但别设太短,模拟器上执行会比真机慢3~5倍。

3.2 断言——测试的“照妖镜”

断言是验证测试结果的核心工具。说白了,就是“我期望A等于B,如果不等就报红”。JUnit4提供了一大组静态断言方法,放在org.junit.Assert类里。

我先列一张常用断言表,都是我们在Android回归用例里高频使用的:

断言方法 用途说明 典型场景
assertEquals(expected, actual) 判断两个值相等(调用equals()方法) 验证返回值、数据库记录字段
assertNotEquals(unexpected, actual) 判断两个值不相等 验证密码修改后新旧密码不同
assertTrue(condition) 判断条件为true 验证登录成功标记位
assertFalse(condition) 判断条件为false 验证错误提示是否消失
assertNull(object) 判断对象为null 验证用户退出后User对象被清除
assertNotNull(object) 判断对象非null 验证SharedPreferences读取结果不为空
assertSame(expected, actual) 判断两个引用指向同一个对象(==) 验证单例模式
assertArrayEquals(expected, actual) 判断两个数组按元素逐一相等 验证图片处理后的像素数组

这里有个坑:assertEquals对于基本类型和包装类型有重载,但如果你传一个int和一个Integer,会自动拆箱,没问题。我曾经在项目中遇到过两个人因为assertEquals报错吵起来,最后发现是浮点数精度问题——assertEquals(double, double)其实已经废弃了,现在要用assertEquals(double, double, delta),给一个容忍误差。你想想看,如果不用delta,两个看似相等的浮点运算结果很可能差一个极小尾数。

// ❌ 错误做法
assertEquals(0.1 + 0.2, 0.3);  // 可能失败

// ✅ 正确做法
assertEquals(0.1 + 0.2, 0.3, 0.0001);

3.3 生命周期——管理测试的前置与后置

每个测试方法都不是孤立运行的。你需要初始化测试对象、清理文件、启动模拟器等。JUnit4用四个注解来管理测试类中方法的执行顺序:

  • @BeforeClass – 在所有测试方法之前执行一次(静态方法)
  • @Before – 在每个测试方法之前执行
  • @After – 在每个测试方法之后执行
  • @AfterClass – 在所有测试方法之后执行一次(静态方法)

下面这张SVG图把整个生命周期串起来了:

JUnit4 测试生命周期流程 @BeforeClass 每个测试方法重复执行 @Before @Test @After 循环 @AfterClass 说明 · @BeforeClass 静态,只1次 · @Before/After 每个测试前后 · @Test 执行 测试业务逻辑 · @AfterClass 静态,最后1次 注意:如果 @Before抛出异常 则@After仍会执行 但@AfterClass不执行

你看这个流程图:@BeforeClass只跑一次,通常用来创建数据库连接、启动模拟器等耗时资源。@Before和@After每个测试方法都要跑一遍,适合做数据初始化、UI重置。最后@AfterClass回收资源。

我刚开始做Android测试时犯过一个错误:在@BeforeClass里启动了一个Activity,然后多个测试方法之间状态互相干扰。后来才意识到,应该在@Before里重置页面状态,或者在每个@Test内部完成一个完整场景。

特别提示:@Before和@After的代码会显著影响测试执行时间。如果你的回归用例集有几百条,每次跑@Before都启动一个Activity,那总时间会爆炸。建议把重量级初始化放在@BeforeClass里,或者使用TestRule来做更精细化的生命周期管理(我们后面会讲到)。

3.4 我曾经踩过的坑——避坑清单

这里总结几个我这些年亲手踩过的坑,希望你别再掉进去。

  • 静态变量污染:@BeforeClass里赋值的静态变量,如果被前面的测试方法改掉了,后面的测试方法会复用这个被污染的状态。所以要确保每个测试方法在执行前,自己负责把共享状态重置一下。
  • 断言信息不明确:很多人写assertEquals(a, b)不提供错误消息。一旦失败,你只看到“expected:true but was:false”,根本不知道哪个变量出了问题。我建议对关键的断言加第三个参数:assertEquals("失败原因", expected, actual)。
  • 超时时间设置不合理:在CI(持续集成)环境中,模拟器加载比本地慢,timeout设得太短会导致回归用例误报。我建议先在本地跑几次,取最大值再乘1.5倍作为安全阈值。
  • 测试方法之间依赖:永远不要假设测试方法按某种顺序执行(就算你设置了@FixMethodOrder),每个@Test必须独立可运行。

就这么简单?对,JUnit4的这三个基础组件,你掌握了就能写出80%的Android回归用例。剩下的20%是技巧和工具集成,比如TestRule、Runner自定义、参数化测试,那些我们后面几章会慢慢展开。