目录
- 什么是单元测试?
- 为什么使用单元测试?
- JUnit 5 核心组件与入门
- Maven/Gradle 依赖配置
- 第一个 JUnit 5 测试
- 核心生命周期注解:
@BeforeEach,@AfterEach,@BeforeAll,@AfterAll
- 核心测试注解
@Test: 定义测试方法@DisplayName: 为测试方法/类设置自定义名称@Disabled: 禁用某个测试
- 核心断言方法
Assertions类详解- 常见断言示例
- 高级测试特性
- 参数化测试 (
@ParameterizedTest) - 假设 (
@Assumptions) - 异常测试 (
assertThrows) - 测试套件 (
@Suite)
- 参数化测试 (
- Mock 测试 (Mockito)
- 什么是 Mock?
- Mockito 简介与集成
- Mock 对象、Stubbing 和 Verification
- 最佳实践
什么是单元测试?
单元测试是针对软件中最小可测试单元(在 Java 中通常是一个方法)进行验证的测试,它的目的是确保每个单元都能按照预期独立、正确地工作。

关键点:
- 最小单元:通常是方法。
- 独立性:每个测试应该独立运行,不依赖其他测试的结果或外部环境(如数据库、网络)。
- 自动化:单元测试应该是自动化的,可以频繁地运行。
为什么使用单元测试?
- 提高代码质量:在编写代码的同时思考如何测试,能促使你写出更清晰、更模块化、更易于维护的代码。
- 快速反馈:当代码逻辑修改后,可以立即运行单元测试来验证改动是否破坏了原有功能,快速定位问题。
- 文档作用:单元测试本身就是一份活的文档,它展示了代码的预期行为和使用方式。
- 重构信心:有了全面的单元测试,你可以放心地对代码进行重构和优化,因为测试会告诉你是否引入了新的错误。
- 简化集成:通过单元测试确保每个模块都正确工作,可以大大降低后期集成测试的难度和成本。
JUnit 5 核心组件与入门
JUnit 5 是目前的主流版本,它由三个模块组成:
- JUnit Jupiter: JUnit 5 的核心引擎,包含新的编程模型和扩展模型(我们主要使用这个)。
- JUnit Vintage: 提供对 JUnit 4 和 JUnit 3 的测试运行支持。
- JUnit Platform: 测试框架的基础,用于启动测试框架和在 JVM 上发现测试。
Maven/Gradle 依赖配置
Maven (pom.xml)
<dependencies>
<!-- JUnit 5 Jupiter 引擎 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.3</version> <!-- 使用最新版本 -->
<scope>test</scope>
</dependency>
</dependencies>
Gradle (build.gradle)

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3' // 使用最新版本
}
// 确保使用 JUnit 5 的测试任务
test {
useJUnitPlatform()
}
第一个 JUnit 5 测试
假设我们有一个简单的 Calculator 类需要测试。
待测试代码 (src/main/java/Calculator.java)
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
测试代码 (src/test/java/CalculatorTest.java)
import org.junit.jupiter.api.Test; // 导入 Test 注解
import static org.junit.jupiter.api.Assertions.assertEquals; // 导入静态断言方法
public class CalculatorTest {
// @Test 注解标记这是一个测试方法
@Test
void testAddition() {
// 1. 准备测试数据
Calculator calculator = new Calculator();
int a = 5;
int b = 3;
int expected = 8;
// 2. 执行测试方法
int result = calculator.add(a, b);
// 3. 验证结果是否符合预期
assertEquals(expected, result, "5 + 3 应该等于 8");
}
@Test
void testSubtraction() {
Calculator calculator = new Calculator();
int a = 10;
int b = 4;
int expected = 6;
int result = calculator.subtract(a, b);
assertEquals(expected, result, "10 - 4 应该等于 6");
}
}
如何运行测试?

- 在 IDE (如 IntelliJ IDEA 或 Eclipse) 中,右键点击
CalculatorTest.java文件,选择 "Run 'CalculatorTest'"。 - 在命令行,使用 Maven:
mvn test或 Gradle:gradle test。
核心生命周期注解
JUnit 提供了生命周期方法,用于在测试执行的不同阶段执行代码。
@BeforeEach: 在每个@Test方法执行之前运行,常用于初始化测试资源。@AfterEach: 在每个@Test方法执行之后运行,常用于清理测试资源。@BeforeAll: 在所有@Test方法执行之前运行,必须是static方法,常用于执行一次性的、昂贵的设置(如连接数据库)。@AfterAll: 在所有@Test方法执行之后运行,必须是static方法,常用于执行一次性的清理(如关闭数据库连接)。
示例:
import org.junit.jupiter.api.*;
class LifecycleExampleTest {
@BeforeAll
static void setupAll() {
System.out.println("在所有测试之前执行一次 (初始化数据库连接)");
}
@AfterAll
static void tearDownAll() {
System.out.println("在所有测试之后执行一次 (关闭数据库连接)");
}
@BeforeEach
void setup() {
System.out.println("在每个测试方法之前执行 (创建测试对象)");
}
@AfterEach
void tearDown() {
System.out.println("在每个测试方法之后执行 (清理测试数据)");
}
@Test
void testOne() {
System.out.println("执行测试方法 testOne");
}
@Test
void testTwo() {
System.out.println("执行测试方法 testTwo");
}
}
执行顺序输出:
在所有测试之前执行一次 (初始化数据库连接)
在每个测试方法之前执行 (创建测试对象)
执行测试方法 testOne
在每个测试方法之后执行 (清理测试数据)
在每个测试方法之前执行 (创建测试对象)
执行测试方法 testTwo
在每个测试方法之后执行 (清理测试数据)
在所有测试之后执行一次 (关闭数据库连接)
核心测试注解
-
@Test: 将一个方法标记为测试用例。 -
@DisplayName: 为测试类或测试方法提供一个更具描述性的名称,这个名称会显示在测试报告中。@Test @DisplayName("测试两个正数相加") void testAddPositiveNumbers() { // ... } -
@Disabled(或@Ignorein JUnit 4): 用于禁用一个测试,测试运行器会跳过它。@Test @Disabled("这个功能暂时不可用,等待修复") void testFeatureThatIsNotReady() { // ... }
核心断言方法
断言是测试的核心,用于验证实际结果是否与预期结果一致,JUnit 5 的断言都位于 org.junit.jupiter.api.Assertions 类中,并且是静态方法,可以直接导入使用。
常用断言方法:
| 方法 | 描述 |
|---|---|
assertEquals(expected, actual) |
验证 actual 是否等于 expected。 |
assertNotEquals(unexpected, actual) |
验证 actual 是否不等于 unexpected。 |
assertTrue(condition) |
验证 condition 是否为 true。 |
assertFalse(condition) |
验证 condition 是否为 false。 |
assertNull(object) |
验证 object 是否为 null。 |
assertNotNull(object) |
验证 object 是否不为 null。 |
assertSame(expected, actual) |
验证 actual 和 expected 是否是同一个对象(引用相等)。 |
assertNotSame(unexpected, actual) |
验证 actual 和 unexpected 是否不是同一个对象。 |
assertThrows(expectedThrowable, executable) |
验证 executable 的执行是否抛出了 expectedThrowable 类型的异常。返回抛出的异常对象,可用于进一步验证。 |
assertDoesNotThrow(executable) |
验证 executable 的执行没有抛出任何异常。 |
示例:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsExampleTest {
@Test
void testAssertions() {
String nullString = null;
String notNullString = "Hello";
// assertEquals
assertEquals(4, 2 + 2, "算术错误");
// assertTrue / assertFalse
assertTrue(5 > 3);
assertFalse(5 < 3);
// assertNull / assertNotNull
assertNull(nullString);
assertNotNull(notNullString);
// assertSame / assertNotSame
Object obj1 = new Object();
Object obj2 = obj1;
Object obj3 = new Object();
assertSame(obj1, obj2);
assertNotSame(obj1, obj3);
// assertThrows
assertThrows(ArithmeticException.class, () -> {
int result = 10 / 0;
}, "除以零应该抛出 ArithmeticException");
// assertThrows 并获取异常对象进行验证
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("参数不能为空");
});
assertEquals("参数不能为空", exception.getMessage());
}
}
高级测试特性
参数化测试 (@ParameterizedTest)
使用 @ParameterizedTest 可以使用不同的参数多次运行同一个测试逻辑,避免代码重复。
需要配合 @ValueSource 等参数源注解使用。
示例:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void testPalindrome(String candidate) {
assertTrue(isPalindrome(candidate));
}
// 一个简单的回文判断方法
private boolean isPalindrome(String str) {
return str.equals(new StringBuilder(str).reverse().toString());
}
@ValueSource 提供基本类型的值数组,JUnit 5 还支持 @EnumSource, @MethodSource, @CsvSource 等更复杂的参数源。
假设 (@Assumptions)
假设允许你根据运行时的条件来动态地启用或禁用测试,如果假设不成立,测试会被跳过,而不是标记为失败。
示例:
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
class AssumptionsExampleTest {
@Test
void testOnlyOnCiServer() {
// 假设我们只在 CI 服务器上运行此测试
boolean isCiServer = "true".equals(System.getenv("CI"));
Assumptions.assumeTrue(isCiServer, "此测试仅在 CI 服务器上运行");
// 只有当 isCiServer 为 true 时,下面的代码才会被执行
System.out.println("测试在 CI 服务器上成功运行");
}
@Test
void testForSpecificEnvironment() {
String environment = System.getProperty("os.name");
Assumptions.assumeFalse(environment.contains("Windows"), "此测试不在 Windows 环境下运行");
System.out.println("测试在非 Windows 环境下成功运行");
}
}
异常测试 (assertThrows)
前面已经提到,这是验证代码是否按预期抛出异常的标准方式。
测试套件 (@Suite)
当你有多个测试类,并希望一次性运行它们时,可以使用测试套件。
步骤:
- 创建一个空的“套件”类。
- 使用
@SelectClasses注解来指定要包含的测试类。
示例:
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
// 测试类 A
class TestA {
@Test @DisplayName("测试 A - 方法 1") void testA1() {}
@Test @DisplayName("测试 A - 方法 2") void testA2() {}
}
// 测试类 B
class TestB {
@Test @DisplayName("测试 B - 方法 1") void testB1() {}
@Test @DisplayName("测试 B - 方法 2") void testB2() {}
}
// 套件类
import org.junit.jupiter.api.api.Suite;
import org.junit.jupiter.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("我的自定义测试套件")
@SelectClasses({TestA.class, TestB.class})
class MyTestSuite {
// 这个类是空的,它只是一个容器
}
Mock 测试 (Mockito)
在单元测试中,我们希望测试的类是孤立的,如果一个类依赖于其他类(如数据库访问、外部服务),我们不希望这些依赖在测试中实际运行,因为它们可能不稳定、缓慢或不可用,这时就需要 Mock。
Mock 是一个模拟对象,你可以预先设定它的行为(调用某个方法时返回什么值),并在测试后验证它是否被正确调用。
Mockito 是 Java 中最流行的 Mock 框架之一。
Mockito 简介
Mockito 允许你:
- 模拟接口或类:创建一个模拟对象。
- Stubbing (存根):当调用模拟对象的某个方法时,让它返回一个预设的值或抛出预设的异常。
- Verification (验证):验证模拟对象的某个方法是否被调用,以及被调用的次数和参数。
Maven/Gradle 依赖配置
Maven (pom.xml)
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.3.1</version> <!-- 使用最新版本 -->
<scope>test</scope>
</dependency>
<!-- 如果要支持 JUnit 5 的扩展,推荐添加这个 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
示例:使用 Mockito
假设我们有一个 UserService,它依赖于一个 UserRepository 来获取用户数据。
待测试代码 (src/main/java/UserService.java)
public class UserService {
private final UserRepository userRepository;
// 通过构造函数注入依赖
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUserNameById(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("ID 必须是正数");
}
User user = userRepository.findById(id);
if (user == null) {
return "用户不存在";
}
return user.getName();
}
}
依赖接口 (src/main/java/UserRepository.java)
public interface UserRepository {
User findById(Long id);
}
实体类 (src/main/java/User.java)
public class User {
private Long id;
private String name;
// ... constructor, getters, setters
}
测试代码 (src/test/java/UserServiceTest.java)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// 1. 使用 @ExtendWith 启用 Mockito 扩展
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
// 2. 创建一个模拟对象,Mockito 会自动管理它的生命周期。
@Mock
private UserRepository userRepository;
// 3. 创建一个真实对象,并将其字段标记为 @Mock 的对象会自动注入进去。
@InjectMocks
private UserService userService;
@Test
void testGetUserNameById_Success() {
// 4. Stubbing (存根): 当调用 userRepository.findById(1L) 时,返回一个预设的 User 对象
User mockUser = new User(1L, "张三");
when(userRepository.findById(1L)).thenReturn(mockUser);
// 5. 执行被测试的方法
String result = userService.getUserNameById(1L);
// 6. 验证结果
assertEquals("张三", result);
// 7. Verification (验证): 验证 userRepository.findById(1L) 确实被调用了一次
verify(userRepository, times(1)).findById(1L);
}
@Test
void testGetUserNameById_UserNotFound() {
// Stubbing: 当调用 userRepository.findById(99L) 时,返回 null
when(userRepository.findById(99L)).thenReturn(null);
String result = userService.getUserNameById(99L);
assertEquals("用户不存在", result);
verify(userRepository).findById(99L);
}
@Test
void testGetUserNameById_InvalidId() {
// 验证当传入无效 ID 时,方法会抛出预期的异常
assertThrows(IllegalArgumentException.class, () -> {
userService.getUserNameById(-1L);
});
// 验证对于无效的 ID,userRepository.findById 根本没有被调用
verify(userRepository, never()).findById(anyLong());
}
}
最佳实践
- 测试名称要清晰:测试方法名应该清晰地描述被测试的功能和场景,
testAdditionWithPositiveNumbers。 - 一个测试只测一件事:每个测试方法应该只验证一个断言或一个核心功能点。
- 保持测试的独立性:避免测试之间有依赖关系,使用
@BeforeEach和@AfterEach来保证每个测试都在一个干净的环境中运行。 - 测试覆盖率不是唯一目标:不要为了追求 100% 的覆盖率而编写无意义的测试(如只测试 getter/setter),专注于测试核心业务逻辑和边界条件。
- 使用
@DisplayName:为测试和测试类起一个有意义的名字,让测试报告更易读。 - F.I.R.S.T 原则:
- Fast (快速):测试应该运行得很快。
- Independent (独立):测试之间不应该相互依赖。
- Repeatable (可重复):无论在什么环境下运行,结果都应该一致。
- Self-Validating (自验证):测试应该通过或失败,不需要人工检查输出。
- Timely (及时):测试代码应该与生产代码同时编写(TDD)或紧跟其后。
JUnit 5 是一个强大、灵活且现代化的测试框架,掌握它是每个 Java 开发者的必备技能,通过编写高质量的单元测试,你可以显著提升代码质量、加速开发流程,并为未来的代码重构提供坚实的保障。
从基础的 @Test 和 assertEquals 开始,逐步探索参数化测试、Mockito 等高级特性,你的测试技能会越来越成熟,测试不是负担,而是提升开发效率和软件质量的强大工具。
