Java基础教程(110)单元测试:解剖Java单元测试,从Mockito魔法到JUnit 5终极实践

为什么需要单元测试?

单元测试是针对程序模块(最小单元)进行的正确性检验,是软件开发的基石。它不仅能极大减少低级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)
    }
}

最佳实践与总结

  1. FIRST原则:测试应遵循Fast(快速)、Independent/Isolated(独立/隔离)、Repeatable(可重复)、Self-Validating(自足验证)、Timely(及时)的原则。
  2. 测试命名:使用MethodName_StateUnderTest_ExpectedBehavior等约定,@DisplayName增强可读性。
  3. 避免过度Mock:只Mock真正的外部依赖(IO、网络),对于稳定的内部数据结构(如值对象),可直接使用真实对象。
  4. 关注行为而非实现:小心使用verify,过度验证会使得测试与实现细节紧耦合,变得脆弱。应更关注输出(返回值/状态改变)而非内部调用。
  5. 单一职责:一个测试方法只测试一个明确的功能点或场景。

通过结合JUnit 5的简洁和Mockito的强大模拟能力,开发者可以构建出高效、可靠且易于维护的单元测试套件,这是交付高质量、可演进软件不可或缺的一环。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值