为什么需要单元测试?
单元测试是针对程序模块(最小单元)进行的正确性检验,是软件开发的基石。它不仅能极大减少低级Bug,更为代码重构提供了坚实的安全网,促进更好的代码设计与可维护性。没有单元测试的项目,其复杂性和缺陷率将随迭代呈指数级增长。
JUnit 5:现代测试框架的核心
JUnit 5由JUnit Platform、JUnit Jupiter和JUnit Vintage组成。其核心注解直观强大:
@Test: 标记一个方法为测试方法。@BeforeEach/@AfterEach: 在每个测试方法运行前/后执行。@BeforeAll/@AfterAll: 在所有测试方法运行前/后执行一次(静态方法)。@DisplayName: 为测试类或方法提供更易读的名称。
基础示例:一个简单的服务测试
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorServiceTest {
private CalculatorService calculator;
@BeforeEach
void setUp() {
calculator = new CalculatorService();
}
@Test
@DisplayName("加法运算应该返回两数之和")
void testAddition() {
assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5");
}
@Test
@DisplayName("除以零时应抛出ArithmeticException异常")
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
}
Mockito:模拟依赖的艺术
真实代码充满依赖(数据库、网络、其他服务)。单元测试应聚焦于当前单元,隔离所有外部依赖。Mockito通过创建mock对象(模拟对象)来实现这一目标。
@Mock: 创建模拟依赖。@InjectMocks: 创建待测试对象,并自动注入其@Mock字段。when().thenReturn(): 设定模拟对象的行为(“打桩”)。verify(): 验证模拟对象的交互是否如预期发生。
进阶示例:模拟依赖的服务层测试
假设有一个UserService,它依赖一个UserRepository(例如数据库访问层)。
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 java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) // 启用Mockito注解
class UserServiceTest {
@Mock
private UserRepository userRepository; // 被模拟的依赖
@InjectMocks
private UserService userService; // 被测试的对象,userRepository自动注入
@Test
@DisplayName("根据ID查找用户 - 用户存在")
void getUserById_UserExists_ReturnsUser() {
// 1. 准备数据 & 定义Mock行为
Long userId = 1L;
User expectedUser = new User(userId, "Alice");
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// 2. 执行待测试的方法
User actualUser = userService.getUserById(userId);
// 3. 断言结果
assertNotNull(actualUser);
assertEquals("Alice", actualUser.getName());
assertEquals(userId, actualUser.getId());
// 4. (可选)验证交互:确认findById被以正确的参数调用了一次
verify(userRepository, times(1)).findById(userId);
}
@Test
@DisplayName("根据ID查找用户 - 用户不存在")
void getUserById_UserNotFound_ThrowsException() {
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> userService.getUserById(userId));
verify(userRepository).findById(userId); // 简写,等同于times(1)
}
}
最佳实践与总结
- FIRST原则:测试应遵循Fast(快速)、Independent/Isolated(独立/隔离)、Repeatable(可重复)、Self-Validating(自足验证)、Timely(及时)的原则。
- 测试命名:使用
MethodName_StateUnderTest_ExpectedBehavior等约定,@DisplayName增强可读性。 - 避免过度Mock:只Mock真正的外部依赖(IO、网络),对于稳定的内部数据结构(如值对象),可直接使用真实对象。
- 关注行为而非实现:小心使用
verify,过度验证会使得测试与实现细节紧耦合,变得脆弱。应更关注输出(返回值/状态改变)而非内部调用。 - 单一职责:一个测试方法只测试一个明确的功能点或场景。
通过结合JUnit 5的简洁和Mockito的强大模拟能力,开发者可以构建出高效、可靠且易于维护的单元测试套件,这是交付高质量、可演进软件不可或缺的一环。

被折叠的 条评论
为什么被折叠?



