%% Mockito核心流程示意图
flowchart TD
    A[数据准备] -->|创建真实对象/模拟对象| B[行为打桩]
    B -->|定义方法返回值/异常| C[执行测试]
    C -->|调用被测方法| D[结果验证]
    D -->|断言/调用验证| E[测试报告]

一、单元测试核心四要素

1.1 完整测试流程解析

sequenceDiagram
    participant T as 测试用例
    participant M as Mock对象
    participant S as 被测系统
    
    T->>M: 数据准备(创建模拟对象)
    T->>M: 行为打桩(定义方法返回值)
    T->>S: 执行被测方法
    S->>M: 调用依赖方法
    M-->>S: 返回预设值
    T->>M: 验证方法调用
    T->>T: 断言结果校验

1.2 核心概念详解

  • 数据准备:创建待测对象及其依赖的模拟对象(真实对象或Mock对象)
  • 打桩(Stubbing):定义模拟对象的方法调用规则(返回值/异常/延时等)
  • 执行测试:触发被测对象的业务方法执行
  • 验证(Verification):验证方法调用次数、参数值和执行顺序

二、环境搭建与配置

2.1 Maven依赖配置

<!-- pom.xml -->
<dependencies>
    <!-- 测试相关依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- Mockito核心库 -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.11.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2 测试类基础模板

@RunWith(MockitoJUnitRunner.class)
public class OrderServiceTest {

    @Mock
    private PaymentDao paymentDao;  // MyBatis Mapper接口

    @Spy
    private InventoryService inventoryService;

    @InjectMocks // @InjectMocks 是 Mockito 框架中用于依赖自动注入的注解,它会将被 @Mock 或 @Spy 修饰的模拟对象自动注入到被测试类的实例中。
    private OrderService orderService;

    @Before
    public void setUp() {
        // MockitoAnnotations.initMocks(this); // 与 @RunWith(MockitoJUnitRunner.class) 的二选一
    }
}

三、基础使用示例

3.1 MyBatis Mapper模拟

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetUserById() {
        // 准备测试数据
        User mockUser = new User(1, "测试用户", "test@example.com");

        // 打桩:当调用selectById(1)时返回模拟用户
        when(userMapper.selectById(1)).thenReturn(mockUser);

        // 执行测试
        UserDTO result = userService.getUserDetail(1);

        // 验证结果
        assertEquals("邮箱匹配", "test@example.com", result.getEmail());

        // 验证Mapper调用
        verify(userMapper, times(1)).selectById(1); // 验证userMapper.selectById是否被调用了1次
    }
}

3.2 HTTP接口模拟

@RunWith(MockitoJUnitRunner.class)
public class WeatherServiceTest {
    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private WeatherService weatherService;

    @Test
    public void testGetWeatherInfo() {
        // 准备模拟响应
        String jsonResponse = "{'temperature':25,'humidity':60}";
        when(restTemplate.getForObject(anyString(), eq(String.class)))
                .thenReturn(jsonResponse);

        // 执行测试
        WeatherDTO weather = weatherService.getCurrentWeather("101010100");

        // 断言验证
        assertNotNull("返回值不为空", weather);
        assertEquals("温度正确", 25, weather.getTemperature());

        // 验证接口调用
        verify(restTemplate).getForObject(
                contains("101010100"),
                eq(String.class)
        );
    }
}

四、核心功能深度解析

4.1 Mock与Spy对比

classDiagram
    class MockObject {
        +所有方法返回默认值
        +需显式定义行为
        +不执行真实方法
    }
    
    class SpyObject {
        +调用真实方法
        +可部分覆盖行为
        +保留对象状态
    }
    
    MockObject <|-- SpyObject : 扩展关系

差异点总结

  • Mock对象:完全虚拟对象,所有方法需显式定义
  • Spy对象:部分真实对象,仅覆盖需要mock的方法
  • 内存占用:Mock对象更轻量,Spy需要维护真实对象状态
  • 执行速度:Mock对象更快,Spy涉及真实方法调用

4.2 高级打桩技巧

// 连续不同返回值
when(mockDao.getCount())
        .thenReturn(10) // 第一次调用返回
    .thenReturn(20) // 第二次调用返回
    .thenThrow(new RuntimeException()); // 第三次调用返回

// 使用 `argThat` 动态匹配参数,只返回符合条件的模拟数据
when(userDao.findByCriteria(
        argThat(c -> c.getAge() > 18)  // 使用 Lambda 表达式定义匹配规则:仅当 age > 18 时匹配
        )).thenReturn(adultUsers);  // 返回符合条件的模拟数据(adultUsers)

// 使用 `thenAnswer` 动态生成返回值,可以访问调用参数并返回自定义响应
when(httpClient.execute(any()))  // 匹配任意 HttpRequest
        .thenAnswer(invocation -> {  // 使用 `invocation` 获取调用参数
HttpRequest request = invocation.getArgument(0);  // 获取第 1 个参数(HttpRequest)
        return buildResponse(request.getUrl());  // 根据 URL 动态生成响应
        });

五、SpringBoot集成测试

5.1 服务层集成测试

@RunWith(SpringRunner.class) // 需要Spring容器
@SpringBootTest
public class ProductServiceIntegrationTest {

    @MockBean // 替换Spring容器中的真实 Bean
    private InventoryMapper inventoryMapper;

    @Autowired
    private ProductService productService;

    @Test
    public void testCheckInventory() {
        // 打桩数据库操作
        when(inventoryMapper.selectStock(1001L))
                .thenReturn(5)
                .thenReturn(0);

        // 第一次调用
        assertTrue(productService.isAvailable(1001L));

        // 第二次调用
        assertFalse(productService.isAvailable(1001L));

        // 验证调用次数
        verify(inventoryMapper, times(2)).selectStock(1001L); // 验证inventoryMapper.selectStock()是否被调用了两次
    }
}

5.2 Controller层测试

@WebMvcTest(OrderController.class)
public class OrderControllerTest {

    @MockBean // 替换Spring容器中的真实 Bean
    private OrderService orderService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testCreateOrder() throws Exception {
        // 准备请求数据
        String jsonBody = "{'productId':1001,'quantity':2}";

        // 打桩服务方法
        when(orderService.createOrder(any()))
                .thenReturn("ORDER_20230801001");

        // 执行HTTP请求
        mockMvc.perform(post("/orders")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(jsonBody))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.orderNo").exists());

        // 验证服务调用
        verify(orderService).createOrder(argThat(order ->
                order.getProductId() == 1001 && order.getQuantity() == 2));
    }
}

六、MyBatis专项Mock

6.1 Mapper接口模拟

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    public void testBatchInsert() {
        // 准备测试数据
        List<User> users = Arrays.asList(
                new User("张三"),
                new User("李四")
        );

        // 打桩插入操作
        when(userMapper.batchInsert(anyList())).thenReturn(2);

        // 执行批量插入
        int result = userService.batchCreateUsers(users);

        // 验证结果
        assertEquals("插入数量正确", 2, result);

        // 验证参数传递
        verify(userMapper).batchInsert(argThat(list ->
                list.size() == 2 &&
                        list.get(0).getName().equals("张三")
        ));
    }
}

6.2 动态SQL验证

@Test
public void testDynamicQuery() {
    // 准备查询条件
    UserQuery query = new UserQuery();
    query.setActive(true);
    query.setMinAge(18);

    // 打桩Mapper方法
    when(userMapper.searchUsers(argThat(q ->
            q.isActive() && q.getMinAge() == 18
    ))).thenReturn(adultUsers);

    // 执行查询
    List<User> result = userService.searchUsers(query);

    // 验证动态SQL生成
    verify(userMapper).searchUsers(argThat(q ->
            q.getQuerySql().contains("WHERE status = 1")
    ));
}

七、常见问题解决方案

7.1 NPE问题处理

问题场景:未初始化Mock对象导致空指针

// 错误示例
@Mock
private UserDao userDao;  // 未初始化

// 正确解决
@Before
public void init() {
    MockitoAnnotations.initMocks(this);
}

7.2 参数验证失败

问题场景:实际参数与预期不匹配

// 精确验证
verify(userDao).updateStatus(
    eq(1001L),     // 用户ID
    eq("ACTIVE"),  // 新状态
    isNotNull()    // 修改时间
);

7.3 静态方法Mock

// 使用Mockito-inline
try (MockedStatic<DateUtils> utilities = Mockito.mockStatic(DateUtils.class)) {
    utilities.when(DateUtils::getCurrentDate)
            .thenReturn(fixedDate);
    
    // 执行测试逻辑...
}