JUnit 5 + Mockito 终极实战笔记

3 阅读3分钟

JUnit 5 + Mockito 终极实战笔记

这份笔记涵盖了从环境搭建、核心概念辨析到复杂场景(Spy、Final类)处理的完整流程。

第一部分:JUnit 4 vs JUnit 5

在 Spring Boot 2.2+ 版本中,JUnit 5 是默认标准。

核心差异速查表

特性JUnit 4 (老旧)JUnit 5 (现代/默认)
架构单体 Jar 包模块化 (Platform + Jupiter)
Test 注解@Test (org.junit.Test)@Test (org.junit.jupiter.api.Test)
生命周期@Before, @After@BeforeEach, @AfterEach
Spring 集成必须写 @RunWith(SpringRunner.class)自动集成,只需 @SpringBootTest
断言Assert.assertEqualsAssertions.assertEquals

环境搭建 (pom.xml)

Spring Boot 的 spring-boot-starter-test 默认已经包含了 JUnit 5 和 Mockito,无需额外配置。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

第二部分:核心概念与优雅测试

Mock 是什么?

Mock 是一个“替身”。它代替真实的数据库或外部服务,让测试只关注当前类的逻辑,而不受外部环境影响(隔离性),且运行速度极快(无需 IO)。

优雅测试的三种策略

  1. 纯单元测试 (推荐 Service 层)

    • 特点:不启动 Spring 容器,速度最快。
    • 注解:@ExtendWith(MockitoExtension.class)
  2. 切片测试 (推荐 Controller 层)

    • 特点:只启动 Web 层容器。
    • 注解:@WebMvcTest
  3. 集成测试 (不推荐频繁使用)

    • 特点:启动完整容器,速度慢。
    • 注解:@SpringBootTest

第三部分:代码实战汇总

假设我们有两个类:

  • UserRepository (依赖,通常是数据库操作)
  • UserService (被测类,包含业务逻辑)

1. 纯单元测试 (使用 @Mock 和 @InjectMocks)

这是最标准、最优雅的写法。

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.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
​
// 1. 启用 Mockito 扩展,无需启动 Spring 容器
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
​
    // 2. 创建一个假的 UserRepository
    @Mock
    private UserRepository userRepository;
​
    // 3. 创建 UserService,并自动把上面的 userRepository 注入进去
    @InjectMocks
    private UserService userService;
​
    @Test
    void shouldFindUserById() {
        // --- Arrange (准备) ---
        // 定义 Mock 行为:当调用 findById(1L) 时,返回一个包含用户的 Optional
        User mockUser = new User(1L, "Alice");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
​
        // --- Act (执行) ---
        User result = userService.findById(1L);
​
        // --- Assert (断言) ---
        assertNotNull(result);
        assertEquals("Alice", result.getName());
​
        // --- Verify (验证) ---
        // 验证 findById 方法是否真的被调用了一次
        verify(userRepository, times(1)).findById(1L);
    }
}

2. 切片测试 (使用 @MockBean)

用于测试 Controller,需要 Spring 容器支持。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
​
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
​
// 1. 只启动 Web 层
@WebMvcTest(UserController.class)
class UserControllerTest {
​
    @Autowired
    private MockMvc mockMvc;
​
    // 2. @MockBean:替换 Spring 容器中的 UserService 为 Mock 对象
    @MockBean
    private UserService userService;
​
    @Test
    void shouldReturnUser() throws Exception {
        // 模拟 Service 层返回数据
        when(userService.findById(1L)).thenReturn(new User(1L, "Alice"));
​
        // 发送 HTTP 请求并验证
        mockMvc.perform(get("/users/1"))
               .andExpect(status().isOk());
    }
}

第四部分:Spy (部分模拟)

当你需要测试真实对象,但想拦截其中某个方法时使用。

关键点

  • 必须手动初始化对象 (= new UserService())。
  • 存根语法必须用 doReturn(...).when(spy)。
@ExtendWith(MockitoExtension.class)
class UserServiceSpyTest {
​
    // 1. 必须手动 new 出来!
    @Spy
    private UserService userService = new UserService();
​
    @Mock
    private UserRepository userRepository;
​
    @Test
    void testSpy() {
        // 2. 注入依赖(Spy 对象通常不会自动注入 Mock,需手动处理或构造)
        // 这里假设我们通过构造器注入了 mock
        userService = new UserService(userRepository); 
        // 重新 Spy 包装(注意:实际开发中建议直接在构造时注入)
        
        // 3. 拦截特定方法
        doReturn("VIP").when(userService).getUserLevel(anyLong());
​
        // 4. 调用真实方法,但 getUserLevel 会返回 "VIP"
        String level = userService.getUserLevel(1L);
        
        assertEquals("VIP", level);
    }
}

第五部分:Final 类 Mock 解决方案

默认 Mockito 无法 Mock final 类。

步骤 1:添加依赖

在 pom.xml 中添加 mockito-inline。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <scope>test</scope>
</dependency>

步骤 2:创建配置文件

在 src/test/resources 下创建文件夹 mockito-extensions,并创建文件 org.mockito.plugins.MockMaker。

文件内容:

mock-maker=inline

步骤 3:直接使用

配置好后,final 类可以直接使用 @Mock 注解,无需任何代码变更。

总结速查

  • @Mock:假的,完全隔离,用于依赖。
  • @InjectMocks:被测对象,自动注入 @Mock。
  • @Spy:真的,但可拦截,用于部分测试。
  • @MockBean:Spring 容器里的假 Bean,用于切片测试。
  • Final 类:加 mockito-inline 依赖和配置文件。

你可以直接鼠标拖拽全选以上所有内容,按 Ctrl+C 就能一键复制啦。

(注:文档部分内容可能由 AI 生成)