JUnit4 完整配置流程
本文档基于
JacocoDemo项目(AGP 8.10.1 + Gradle Kotlin DSL + Java 11),详细说明如何在 Android 项目中配置和使用 JUnit4 进行本地单元测试。
一、JUnit4 是什么?
JUnit4 是 Java 生态中最流行的单元测试框架。在 Android 项目中,它主要用于 本地单元测试(Local Unit Test)------在本地 JVM 上快速运行,不依赖 Android 设备和模拟器。
JUnit4 在 Android 项目中的作用
- 快速验证业务逻辑:无需启动设备,秒级执行
- 测试纯 Java/Kotlin 代码:Utils、Model、ViewModel(配合 Robolectric)等
- 与 JaCoCo 集成:生成代码覆盖率报告
- CI/CD 集成:在持续集成流水线中自动执行
二、通过版本目录声明依赖
本项目使用 Gradle Version Catalog(版本目录)管理依赖版本,配置文件位于项目根目录的 gradle/libs.versions.toml。
2.1 在 libs.versions.toml 中声明版本和依赖
toml
[versions]
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
[libraries]
# 本地单元测试:JUnit 4
junit = { group = "junit", name = "junit", version.ref = "junit" }
# 仪器化测试(Android 设备测试):AndroidX Test JUnit
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
# UI 测试:Espresso
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
2.2 版本目录的优势
- 集中管理:所有版本定义在一个文件中
- 引用统一 :通过
version.ref确保版本一致性 - 便于升级:修改一处即可全局生效
三、在 app/build.gradle.kts 中引用依赖
3.1 依赖声明
kotlin
dependencies {
// 本地单元测试依赖(运行在本地 JVM)
testImplementation(libs.junit)
// 仪器化测试依赖(运行在 Android 设备/模拟器)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}
3.2 testImplementation 与 androidTestImplementation 的区别
| 配置指令 | 运行环境 | 测试目录 | 典型用途 |
|---|---|---|---|
testImplementation |
本地 JVM(电脑) | src/test/java |
纯逻辑测试、工具类测试 |
androidTestImplementation |
Android 设备/模拟器 | src/androidTest/java |
UI 测试、需要 Android Framework 的测试 |
核心区别:
testImplementation的测试 不依赖 Android 框架,运行速度极快(毫秒级)androidTestImplementation的测试 需要 Android 运行时环境,运行速度较慢(秒级)
四、测试目录结构说明
bash
app/
└── src/
├── main/java/ # 主源代码
│ └── com/android/jacocodemo/
│ ├── MainActivity.java
│ └── util/
│ └── ScoreUtil.java
│
├── test/java/ # 本地单元测试(本地 JVM 运行)
│ └── com/android/jacocodemo/
│ ├── ExampleUnitTest.java
│ └── ScoreUtilTest.java
│
└── androidTest/java/ # 仪器化测试(Android 设备运行)
└── com/android/jacocodemo/
└── ExampleInstrumentedTest.java
目录对比
| 目录 | 完整路径 | 运行环境 | Gradle Task |
|---|---|---|---|
| 本地单测 | src/test/java |
本地 JVM | testDebugUnitTest |
| 仪器化测试 | src/androidTest/java |
Android 设备 | connectedAndroidTest |
五、编写测试类的完整步骤
5.1 基本结构
一个标准的 JUnit4 测试类:
java
import org.junit.Before;
import org.junit.After;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import static org.junit.Assert.*;
public class MyTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
// 每个测试方法执行前运行
}
@After
public void tearDown() {
// 每个测试方法执行后运行
}
@Test
public void testSomething() {
// 测试逻辑
}
}
5.2 核心注解说明
| 注解 | 说明 | 执行时机 |
|---|---|---|
@Test |
标记测试方法 | 被测试运行器调用 |
@Before |
前置操作 | 每个 @Test 方法执行前 |
@After |
后置操作 | 每个 @Test 方法执行后 |
@BeforeClass |
类级别前置 | 所有测试执行前(需 static) |
@AfterClass |
类级别后置 | 所有测试执行后(需 static) |
@Ignore |
跳过测试 | 标记后该方法不会被执行 |
@Rule |
测试规则 | 提供额外的测试功能 |
@ClassRule |
类级别规则 | 作用于整个测试类 |
5.3 常用 @Rule
java
// 1. 预期异常规则
@Rule
public ExpectedException thrown = ExpectedException.none();
// 使用示例
@Test
public void testException() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("不能为负数");
ScoreUtil.calculate(-1);
}
// 2. 临时目录规则
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
// 3. 超时规则
@Rule
public Timeout timeout = Timeout.seconds(5);
5.4 项目示例:ExampleUnitTest
本项目中的 app/src/test/java/com/android/jacocodemo/ExampleUnitTest.java:
java
package com.android.jacocodemo;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* 本地单元测试示例
* 运行在本地 JVM,不依赖 Android 设备
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
要点:
- 使用
@Test注解标记测试方法 - 使用
assertEquals进行断言 - 方法名无强制要求,但建议描述测试意图
5.5 项目示例:ScoreUtilTest
本项目中的 app/src/test/java/com/android/jacocodemo/ScoreUtilTest.java:
java
package com.android.jacocodemo;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* ScoreUtil 工具类单元测试
* 演示 @Before 初始化、分支覆盖测试
*/
public class ScoreUtilTest {
private ScoreUtil scoreUtil;
@Before
public void setUp() {
// 每个测试方法执行前创建新实例,保证测试隔离
scoreUtil = new ScoreUtil();
}
@Test
public void getGrade_score90以上_返回A() {
assertEquals("A", scoreUtil.getGrade(95));
}
@Test
public void getGrade_score80到89_返回B() {
assertEquals("B", scoreUtil.getGrade(85));
}
@Test
public void getGrade_score60到79_返回C() {
assertEquals("C", scoreUtil.getGrade(70));
}
@Test
public void getGrade_score低于60_返回D() {
assertEquals("D", scoreUtil.getGrade(45));
}
@Test(expected = IllegalArgumentException.class)
public void getGrade_负数分数_抛出异常() {
scoreUtil.getGrade(-1);
}
@Test(expected = IllegalArgumentException.class)
public void getGrade_超过100_抛出异常() {
scoreUtil.getGrade(101);
}
}
要点:
@Before在每个测试前重置对象状态- 覆盖多个分支(A/B/C/D 等级 + 异常边界)
- 使用
expected属性测试异常抛出 - 测试方法名采用
方法名_条件_期望结果命名规范
六、断言方法说明
JUnit4 提供在 org.junit.Assert 类中的断言方法:
6.1 基础断言
| 方法 | 说明 | 示例 |
|---|---|---|
assertEquals(expected, actual) |
断言两个值相等 | assertEquals(4, 2 + 2) |
assertNotEquals(unexpected, actual) |
断言两个值不相等 | assertNotEquals(5, 2 + 2) |
assertTrue(condition) |
断言条件为真 | assertTrue(list.isEmpty()) |
assertFalse(condition) |
断言条件为假 | assertFalse(str.isEmpty()) |
assertNull(object) |
断言对象为 null | assertNull(result) |
assertNotNull(object) |
断言对象不为 null | assertNotNull(result) |
assertSame(expected, actual) |
断言同一对象(==) | assertSame(a, b) |
assertNotSame(unexpected, actual) |
断言不同对象 | assertNotSame(a, b) |
6.2 带消息的断言
所有断言方法都支持第一个参数为失败消息:
java
assertEquals("分数等级应为 A", "A", scoreUtil.getGrade(95));
assertTrue("列表不应为空", !list.isEmpty());
assertNotNull("结果不应为 null", result);
6.3 数组和集合断言
java
// 数组相等(内容比较)
assertArrayEquals(new int[]{1, 2, 3}, result);
// 集合内容相等
assertThat(list, hasItems("a", "b", "c")); // 需 Hamcrest 库
6.4 异常测试
java
// 方式一:@Test 的 expected 属性
@Test(expected = IllegalArgumentException.class)
public void testException() {
scoreUtil.getGrade(-1);
}
// 方式二:ExpectedException 规则(可检查异常消息)
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testExceptionWithMessage() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("不能为负数");
scoreUtil.getGrade(-1);
}
七、运行测试
7.1 通过 Gradle 命令运行
bash
# 运行所有本地单元测试
gradlew testDebugUnitTest
# 查看测试结果
# 报告路径:app/build/reports/tests/testDebugUnitTest/index.html
7.2 测试输出
测试执行后,结果文件位于:
bash
app/build/
├── reports/tests/testDebugUnitTest/ # HTML 格式测试报告
├── test-results/testDebugUnitTest/ # XML 格式测试结果
└── outputs/unit_test_code_coverage/ # JaCoCo 覆盖率数据(已开启时)
八、常见问题
Q1:测试找不到 Android 资源(R.string.xxx)?
本地单元测试运行在纯 JVM 环境,不加载 Android 资源。解决方案:
kotlin
// app/build.gradle.kts 中已配置
testOptions {
unitTests.isIncludeAndroidResources = true // 包含 Android 资源
unitTests.isReturnDefaultValues = true // Android 方法返回默认值
}
Q2:如何测试需要 Context 的代码?
- 方案一:重构代码,将需要 Context 的逻辑抽取为可注入的接口
- 方案二:使用 Mockito 模拟 Context
- 方案三:使用 Robolectric 框架(在本地模拟 Android 环境)
Q3:@Before 和 @BeforeClass 有什么区别?
| 注解 | 执行频率 | 方法要求 | 适用场景 |
|---|---|---|---|
@Before |
每个测试方法前 | 实例方法 | 重置测试数据 |
@BeforeClass |
整个类只执行一次 | 静态方法 | 初始化共享资源 |
九、总结
| 配置项 | 本项目实际值 |
|---|---|
| JUnit 版本 | 4.13.2 |
| 版本目录文件 | gradle/libs.versions.toml |
| 本地测试依赖 | testImplementation(libs.junit) |
| 仪器化测试依赖 | androidTestImplementation(libs.ext.junit) |
| 本地测试目录 | src/test/java |
| 仪器化测试目录 | src/androidTest/java |
| Gradle 测试 Task | testDebugUnitTest |