3、JUnit4/5基础:JUnit注解详解、断言机制、测试套件、参数化测试、测试执行顺序
好,咱们进入正题。JUnit 这东西,说白了就是 Android 自动化测试的基石。不管你用 Espresso 还是 UI Automator,底层跑的都是 JUnit。我刚开始带团队的时候,发现很多同学写测试用例就是「能用就行」,注解乱用、断言瞎写,结果 CI 跑起来一片红,排查问题比写代码还累。今天我就把 JUnit4 和 JUnit5 的核心知识点掰开揉碎了讲清楚。
3.1 JUnit 注解详解:从 @Test 到 @DisplayName
注解是 JUnit 的灵魂。你想想看,没有注解,框架怎么知道哪个方法是测试方法?哪个方法要在测试前执行?
3.1.1 JUnit4 核心注解
| 注解 | 作用 | 执行时机 |
|---|---|---|
| @Test | 标记一个方法为测试方法 | 运行时 |
| @Before | 每个测试方法执行前运行 | 每个 @Test 前 |
| @After | 每个测试方法执行后运行 | 每个 @Test 后 |
| @BeforeClass | 所有测试方法执行前运行一次(静态方法) | 类加载时 |
| @AfterClass | 所有测试方法执行后运行一次(静态方法) | 类销毁时 |
| @Ignore | 忽略该测试方法 | 不执行 |
我在项目中遇到过一个问题:有个同事把数据库初始化的代码写在 @Before 里,结果每个测试用例都重新建表,跑 100 个用例花了 10 分钟。后来改成 @BeforeClass,一次初始化,30 秒跑完。嗯,这里要注意,@BeforeClass 必须是静态方法,而且不能依赖实例变量。
3.1.2 JUnit5 新注解
JUnit5 改了不少东西。我个人习惯用 JUnit5 写新项目,因为它的注解更语义化。
| JUnit4 | JUnit5 | 变化说明 |
|---|---|---|
| @Test | @Test | 功能相同,但 JUnit5 的 @Test 来自 org.junit.jupiter.api |
| @Before | @BeforeEach | 名字更清晰:每个测试前执行 |
| @After | @AfterEach | 名字更清晰:每个测试后执行 |
| @BeforeClass | @BeforeAll | 所有测试前执行一次 |
| @AfterClass | @AfterAll | 所有测试后执行一次 |
| @Ignore | @Disabled | 语义更明确:禁用而非忽略 |
| 无 | @DisplayName | 给测试类或方法起中文名,报告更友好 |
重点:@DisplayName 是我最喜欢的新特性。以前看测试报告,一堆 testLoginWithValidCredentials 这种驼峰命名,眼睛都看花了。加上 @DisplayName("登录测试-有效凭证"),报告瞬间可读性拉满。
// JUnit5 示例
@DisplayName("用户登录模块测试")
class LoginTest {
@BeforeEach
void setUp() {
// 每个测试前初始化
}
@Test
@DisplayName("使用有效凭证登录应该成功")
void testLoginWithValidCredentials() {
// 测试逻辑
}
@Test
@Disabled("该功能尚未实现,暂时跳过")
void testLoginWithBiometric() {
// 指纹登录测试
}
}
3.2 断言机制:让测试自己会说话
断言是测试的「裁判」。没有断言,测试跑完你也不知道对不对。我见过最离谱的测试用例,整个方法就一行 System.out.println("测试通过")——这能叫测试?
3.2.1 JUnit4 断言
JUnit4 的断言都在 org.junit.Assert 类里,全是静态方法。
import static org.junit.Assert.*;
@Test
public void testAssertions() {
// 基本断言
assertEquals("值不相等", 4, 2 + 2);
assertTrue("条件应为真", 5 > 3);
assertFalse("条件应为假", 3 > 5);
assertNull("应为空", null);
assertNotNull("不应为空", "hello");
// 数组断言
int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals("数组不相等", expected, actual);
// 异常断言
@Test(expected = IllegalArgumentException.class)
public void testException() {
throw new IllegalArgumentException("参数错误");
}
}
避坑指南:我曾经在项目里用 assertEquals 比较两个浮点数,结果 0.1 + 0.2 不等于 0.3。为什么?浮点数精度问题。后来改用 assertEquals(0.3, 0.1 + 0.2, 0.0001),第三个参数是 delta 误差范围,这才搞定。
3.2.2 JUnit5 断言
JUnit5 把断言移到了 org.junit.jupiter.api.Assertions,并且增加了 lambda 支持。
import static org.junit.jupiter.api.Assertions.*;
@Test
void testAssertions() {
// 基本断言(支持 lambda 消息)
assertEquals(4, 2 + 2, () -> "计算错误:2+2 应该等于 4");
// 分组断言
assertAll("用户属性验证",
() -> assertEquals("张三", user.getName()),
() -> assertEquals(25, user.getAge()),
() -> assertTrue(user.isActive())
);
// 异常断言(更优雅)
assertThrows(IllegalArgumentException.class, () -> {
new User(-1);
}, "年龄为负数时应抛出异常");
// 超时断言
assertTimeout(Duration.ofMillis(100), () -> {
// 必须在100ms内完成
someMethod();
});
}
我的习惯:assertAll 特别好用。以前写多个断言,第一个失败了后面的就不跑了,排查问题得反复跑。用 assertAll 会把所有失败都列出来,一次跑完所有断言,省时省力。
3.3 测试套件:批量执行的艺术
测试多了以后,你不可能一个个点着跑。测试套件就是用来批量组织测试的。
3.3.1 JUnit4 测试套件
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
LoginTest.class,
RegisterTest.class,
ProfileTest.class
})
public class AllTestsSuite {
// 这个类不需要任何方法
}
跑这个套件,三个测试类会依次执行。我在 CI 流程里就是这么干的——把单元测试、集成测试、UI 测试分别打包成不同的套件,按阶段执行。
3.3.2 JUnit5 测试套件
JUnit5 用 @Suite 注解,需要额外依赖 junit-platform-suite-api。
import org.junit.platform.suite.api.*;
@Suite
@SelectPackages("com.example.tests") // 扫描整个包
@IncludeTags("smoke") // 只跑 smoke 标签的测试
@ExcludeTags("slow") // 排除慢测试
public class SmokeTestSuite {
// 空类即可
}
实战经验:我习惯把测试分成三个套件:冒烟测试(核心功能,每次提交必跑)、回归测试(全量,每晚跑)、性能测试(每周跑一次)。这样 CI 流程既快又稳。
3.4 参数化测试:一个测试跑 N 组数据
你想想看,测试登录功能,你得测用户名正确、密码正确、用户名错误、密码错误、都错误……难道写 5 个测试方法?参数化测试就是干这个的。
3.4.1 JUnit4 参数化测试
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
import java.util.Collection;
@RunWith(Parameterized.class)
public class CalculatorTest {
private int a;
private int b;
private int expected;
// 构造函数注入参数
public CalculatorTest(int a, int b, int expected) {
this.a = a;
this.b = b;
this.expected = expected;
}
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{1, 1, 2},
{2, 3, 5},
{5, 5, 10},
{10, -5, 5}
});
}
@Test
public void testAdd() {
assertEquals(expected, a + b);
}
}
说实话,JUnit4 的参数化有点麻烦。你得写构造函数、写静态数据源方法,代码量不小。
3.4.2 JUnit5 参数化测试
JUnit5 就优雅多了。它用 @ParameterizedTest 配合各种数据源注解。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class CalculatorTest {
// 方式一:直接写值
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testPositive(int number) {
assertTrue(number > 0);
}
// 方式二:CSV 格式
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 5, 10"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, a + b);
}
// 方式三:从 CSV 文件读取
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testFromFile(int a, int b, int expected) {
assertEquals(expected, a + b);
}
// 方式四:枚举
@ParameterizedTest
@EnumSource(Month.class)
void testMonth(Month month) {
assertNotNull(month);
}
// 方式五:方法返回值
@ParameterizedTest
@MethodSource("dataProvider")
void testFromMethod(int a, int b, int expected) {
assertEquals(expected, a + b);
}
static Stream<Arguments> dataProvider() {
return Stream.of(
Arguments.of(1, 1, 2),
Arguments.of(2, 3, 5)
);
}
}
我的建议:项目里数据量少用 @CsvSource,数据量多用 @CsvFileSource。我曾经把 200 组测试数据写在 @CsvSource 里,代码直接爆炸。后来改成 CSV 文件,清爽多了。
3.5 测试执行顺序:别让顺序坑了你
测试应该是独立的、无序的。但现实是,有时候你不得不控制顺序。比如集成测试里,你得先注册才能登录。
3.5.1 JUnit4 执行顺序
JUnit4 默认不保证执行顺序。想控制顺序?用 @FixMethodOrder。
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class OrderedTest {
@Test
public void testA_register() {
// 先注册
}
@Test
public void testB_login() {
// 后登录
}
@Test
public void testC_logout() {
// 最后退出
}
}
NAME_ASCENDING 按方法名排序。所以我把方法名写成 testA、testB、testC,按字母顺序执行。
3.5.2 JUnit5 执行顺序
JUnit5 用 @TestMethodOrder 注解,更灵活。
import org.junit.jupiter.api.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {
@Test
@Order(3)
void testLogout() {
// 第三步
}
@Test
@Order(1)
void testRegister() {
// 第一步
}
@Test
@Order(2)
void testLogin() {
// 第二步
}
}
曾经踩过的坑:有次我依赖测试顺序,结果 CI 环境换了 JDK 版本,方法名的排序变了,测试全挂。后来我改成 @Order 注解,明确指定顺序,再也没出过问题。记住:依赖顺序的测试是坏味道,能避免就避免。
3.6 实战总结
好了,这一章的内容不少。我帮你理一下重点:
- 注解:JUnit5 的 @BeforeEach/@AfterEach 比 JUnit4 的 @Before/@After 更语义化,推荐用新不用旧
- 断言:assertAll 和 assertThrows 是 JUnit5 的杀手锏,能大幅提升测试质量
- 套件:按冒烟、回归、性能分层组织,CI 流程更高效
- 参数化:一个测试跑多组数据,@CsvFileSource 适合大数据量场景
- 顺序:能用 @Order 就别依赖字母排序,明确指定最安全
下一章咱们会讲 Mockito 和 PowerMock,到时候这些 JUnit 知识全都会用上。你先把这些基础打牢,后面写起测试来才能得心应手。