Mockito 单元测试框架实战

50 阅读8分钟

Mockito 单元测试框架实战

在这里插入图片描述

一、Mockito 简介

Mockito 是 Java 生态中最流行的 Mock 框架,被广泛应用于单元测试中。它提供了简洁优雅的 API,帮助开发者轻松创建和配置 Mock 对象,从而实现对依赖项的隔离测试。

1.1 为什么需要 Mock?

在实际开发中,我们常常遇到以下场景:

  • 依赖的外部服务(数据库、HTTP API)难以在测试环境搭建
  • 某些依赖执行缓慢,影响测试效率
  • 需要模拟各种异常情况进行测试
  • 希望独立测试某个模块,不受其他模块影响

Mockito 正是为解决这些问题而生,它可以创建虚拟对象替代真实依赖,让测试变得简单高效。

1.2 Mockito 的核心优势

  • 简洁的 API:使用流畅的链式调用,代码可读性强
  • 强大的验证功能:可以验证方法调用次数、参数等
  • 灵活的参数匹配:支持精确匹配、类型匹配、自定义匹配
  • 注解支持:通过注解简化 Mock 对象的创建
  • 与主流框架集成:完美支持 JUnit、Spring Boot 等

二、Mockito 核心架构

在这里插入图片描述

Mockito 的架构分为四个主要层次:

  1. 测试代码层:我们编写的测试代码
  2. Mockito API:提供 mock()、when()、verify() 等核心方法
  3. Mock 对象代理:动态创建的代理对象
  4. 字节码增强:底层使用 ByteBuddy 进行字节码操作

三、快速开始

3.1 添加依赖

在项目中添加 Mockito 依赖:

<!-- Maven -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.8.0</version>
    <scope>test</scope>
</dependency>

<!-- 如果使用 JUnit 5 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.8.0</version>
    <scope>test</scope>
</dependency>
// Gradle
testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.8.0'

3.2 基本用法三步曲

在这里插入图片描述

Mockito 的基本使用可以归纳为三个步骤:

步骤 1:创建 Mock 对象
import static org.mockito.Mockito.*;

// 方式一:使用 mock() 方法
List<String> mockList = mock(List.class);

// 方式二:使用 @Mock 注解
@Mock
private UserRepository userRepository;
步骤 2:定义 Mock 行为
// 定义返回值
when(mockList.get(0)).thenReturn("first");
when(mockList.size()).thenReturn(10);

// 定义异常
when(mockList.get(1)).thenThrow(new RuntimeException("出错了"));

// 链式调用
when(mockList.isEmpty())
    .thenReturn(false)
    .thenReturn(true);  // 第二次调用返回 true
步骤 3:验证方法调用
// 验证方法被调用
verify(mockList).add("item");

// 验证调用次数
verify(mockList, times(2)).add(anyString());

// 验证从未调用
verify(mockList, never()).clear();

四、验证方法详解

在这里插入图片描述

Mockito 提供了丰富的验证方法来检查 Mock 对象的交互:

4.1 调用次数验证

import static org.mockito.Mockito.*;

@Test
public void testVerification() {
    List<String> mockList = mock(List.class);

    mockList.add("once");
    mockList.add("twice");
    mockList.add("twice");

    // 验证调用 1 次(默认)
    verify(mockList).add("once");

    // 验证调用 2 次
    verify(mockList, times(2)).add("twice");

    // 验证从未调用
    verify(mockList, never()).clear();

    // 验证至少调用 1 次
    verify(mockList, atLeastOnce()).add(anyString());

    // 验证至少调用 2 次
    verify(mockList, atLeast(2)).add("twice");

    // 验证最多调用 5 次
    verify(mockList, atMost(5)).add(anyString());
}

4.2 调用顺序验证

@Test
public void testInOrder() {
    List<String> firstMock = mock(List.class);
    List<String> secondMock = mock(List.class);

    firstMock.add("first");
    secondMock.add("second");
    firstMock.add("third");

    // 验证调用顺序
    InOrder inOrder = inOrder(firstMock, secondMock);
    inOrder.verify(firstMock).add("first");
    inOrder.verify(secondMock).add("second");
    inOrder.verify(firstMock).add("third");
}

4.3 超时验证

@Test
public void testTimeout() {
    List<String> mockList = mock(List.class);

    // 异步调用
    new Thread(() -> {
        try {
            Thread.sleep(100);
            mockList.add("async");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    // 验证在 200ms 内被调用
    verify(mockList, timeout(200)).add("async");
}

五、参数匹配器

在这里插入图片描述

参数匹配器(ArgumentMatchers)让我们可以灵活地匹配方法参数。

5.1 常用匹配器

import static org.mockito.ArgumentMatchers.*;

@Test
public void testMatchers() {
    Map<String, String> mockMap = mock(Map.class);

    // any() - 匹配任何对象
    when(mockMap.get(any())).thenReturn("value");

    // anyString() - 匹配任何字符串
    when(mockMap.put(anyString(), anyString())).thenReturn("old");

    // anyInt() - 匹配任何整数
    when(mockMap.remove(anyInt())).thenReturn("removed");

    // eq() - 精确匹配
    when(mockMap.get(eq("key"))).thenReturn("specific");

    // isNull() - 匹配 null
    when(mockMap.get(isNull())).thenReturn("null-value");

    // isNotNull() - 匹配非 null
    when(mockMap.containsKey(isNotNull())).thenReturn(true);
}

5.2 自定义匹配器

@Test
public void testCustomMatcher() {
    List<String> mockList = mock(List.class);

    // 使用 argThat 自定义匹配逻辑
    when(mockList.add(argThat(s -> s.length() > 5)))
        .thenReturn(true);

    assertTrue(mockList.add("long string"));  // true
    assertFalse(mockList.add("short"));       // false
}

5.3 混合使用注意事项

// ❌ 错误:不能混用匹配器和具体值
when(mockMap.put(anyString(), "value")).thenReturn("old");

// ✅ 正确:全部使用匹配器
when(mockMap.put(anyString(), eq("value"))).thenReturn("old");

六、高级特性

在这里插入图片描述

6.1 使用注解简化代码

Mockito 提供了多个注解来简化 Mock 对象的创建和注入:

import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    // @Mock 创建 Mock 对象
    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    // @InjectMocks 自动注入 Mock 依赖
    @InjectMocks
    private UserService userService;

    @Test
    public void testCreateUser() {
        User user = new User("张三", "zhang@example.com");
        when(userRepository.save(any(User.class))).thenReturn(user);

        User created = userService.createUser(user);

        verify(userRepository).save(user);
        verify(emailService).sendWelcomeEmail(user.getEmail());
        assertEquals("张三", created.getName());
    }
}

6.2 Spy - 部分 Mock

Spy 允许我们监视真实对象,只 Mock 部分方法:

@Test
public void testSpy() {
    List<String> realList = new ArrayList<>();
    List<String> spyList = spy(realList);

    // 真实方法会被调用
    spyList.add("one");
    spyList.add("two");

    // Mock 特定方法
    when(spyList.size()).thenReturn(100);

    assertEquals(100, spyList.size());  // Mock 的行为
    assertEquals("one", spyList.get(0));  // 真实的行为
}

实际应用场景

public class OrderService {

    public double calculateTotal(Order order) {
        double subtotal = calculateSubtotal(order);
        double tax = calculateTax(subtotal);
        return subtotal + tax;
    }

    protected double calculateSubtotal(Order order) {
        // 复杂的计算逻辑
        return order.getItems().stream()
            .mapToDouble(Item::getPrice)
            .sum();
    }

    protected double calculateTax(double amount) {
        return amount * 0.1;
    }
}

@Test
public void testCalculateTotal() {
    OrderService orderService = spy(new OrderService());

    // 只 Mock calculateTax 方法
    doReturn(50.0).when(orderService).calculateTax(anyDouble());

    Order order = new Order();
    order.addItem(new Item("商品", 100.0));

    double total = orderService.calculateTotal(order);

    assertEquals(150.0, total);  // 100 + 50
}

6.3 Answer - 自定义返回逻辑

Answer 接口允许我们实现复杂的返回逻辑:

@Test
public void testAnswer() {
    List<String> mockList = mock(List.class);

    // 根据参数动态返回结果
    when(mockList.get(anyInt())).thenAnswer(invocation -> {
        Integer index = invocation.getArgument(0);
        return "element-" + index;
    });

    assertEquals("element-0", mockList.get(0));
    assertEquals("element-5", mockList.get(5));
}

实际应用场景 - 模拟异步回调

public interface AsyncService {
    void executeAsync(String task, Callback callback);
}

@Test
public void testAsyncCallback() {
    AsyncService asyncService = mock(AsyncService.class);

    doAnswer(invocation -> {
        String task = invocation.getArgument(0);
        Callback callback = invocation.getArgument(1);

        // 模拟异步执行后调用回调
        callback.onSuccess("完成: " + task);
        return null;
    }).when(asyncService).executeAsync(anyString(), any(Callback.class));

    Callback mockCallback = mock(Callback.class);
    asyncService.executeAsync("任务1", mockCallback);

    verify(mockCallback).onSuccess("完成: 任务1");
}

6.4 ArgumentCaptor - 捕获参数

ArgumentCaptor 用于捕获传递给 Mock 方法的参数,便于后续断言:

@Test
public void testArgumentCaptor() {
    List<String> mockList = mock(List.class);

    mockList.add("John");
    mockList.add("Jane");

    // 创建 ArgumentCaptor
    ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);

    // 捕获所有调用的参数
    verify(mockList, times(2)).add(captor.capture());

    List<String> capturedValues = captor.getAllValues();
    assertEquals(Arrays.asList("John", "Jane"), capturedValues);
}

实际应用场景 - 验证复杂对象

@Test
public void testCaptureComplexObject() {
    UserRepository userRepository = mock(UserRepository.class);
    UserService userService = new UserService(userRepository);

    userService.registerUser("张三", "zhang@example.com", 25);

    // 捕获传递给 save 方法的 User 对象
    ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
    verify(userRepository).save(userCaptor.capture());

    User capturedUser = userCaptor.getValue();
    assertEquals("张三", capturedUser.getName());
    assertEquals("zhang@example.com", capturedUser.getEmail());
    assertEquals(25, capturedUser.getAge());
    assertNotNull(capturedUser.getCreatedAt());
}

七、Spring Boot 集成实战

在实际项目中,Mockito 最常见的应用场景是测试 Spring Boot 应用。

7.1 依赖配置

Spring Boot 项目通常已经包含 Mockito,但建议显式添加:

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

7.2 Service 层测试

被测试的 Service

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final NotificationService notificationService;

    @Autowired
    public OrderService(OrderRepository orderRepository,
                       InventoryService inventoryService,
                       PaymentService paymentService,
                       NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.notificationService = notificationService;
    }

    public Order createOrder(OrderRequest request) {
        // 1. 检查库存
        if (!inventoryService.checkStock(request.getProductId(), request.getQuantity())) {
            throw new InsufficientStockException("库存不足");
        }

        // 2. 创建订单
        Order order = new Order();
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setUserId(request.getUserId());
        order.setStatus(OrderStatus.PENDING);
        order.setCreatedAt(LocalDateTime.now());

        Order savedOrder = orderRepository.save(order);

        // 3. 扣减库存
        inventoryService.decreaseStock(request.getProductId(), request.getQuantity());

        // 4. 处理支付
        PaymentResult paymentResult = paymentService.processPayment(
            savedOrder.getId(),
            request.getPaymentMethod(),
            calculateAmount(request)
        );

        if (paymentResult.isSuccess()) {
            savedOrder.setStatus(OrderStatus.PAID);
            orderRepository.save(savedOrder);

            // 5. 发送通知
            notificationService.sendOrderConfirmation(savedOrder);
        } else {
            // 回滚库存
            inventoryService.increaseStock(request.getProductId(), request.getQuantity());
            savedOrder.setStatus(OrderStatus.FAILED);
        }

        return savedOrder;
    }

    private BigDecimal calculateAmount(OrderRequest request) {
        // 计算金额逻辑
        return BigDecimal.valueOf(request.getQuantity() * 100);
    }
}

测试类

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderService orderService;

    private OrderRequest orderRequest;

    @BeforeEach
    void setUp() {
        orderRequest = new OrderRequest();
        orderRequest.setProductId(1L);
        orderRequest.setQuantity(2);
        orderRequest.setUserId(100L);
        orderRequest.setPaymentMethod("CREDIT_CARD");
    }

    @Test
    @DisplayName("成功创建订单")
    void testCreateOrder_Success() {
        // Given
        when(inventoryService.checkStock(1L, 2)).thenReturn(true);

        Order mockOrder = new Order();
        mockOrder.setId(1L);
        mockOrder.setStatus(OrderStatus.PENDING);
        when(orderRepository.save(any(Order.class))).thenReturn(mockOrder);

        PaymentResult paymentResult = new PaymentResult(true, "SUCCESS");
        when(paymentService.processPayment(anyLong(), anyString(), any(BigDecimal.class)))
            .thenReturn(paymentResult);

        // When
        Order result = orderService.createOrder(orderRequest);

        // Then
        assertNotNull(result);
        assertEquals(OrderStatus.PAID, result.getStatus());

        // 验证交互
        verify(inventoryService).checkStock(1L, 2);
        verify(inventoryService).decreaseStock(1L, 2);
        verify(orderRepository, times(2)).save(any(Order.class));
        verify(paymentService).processPayment(eq(1L), eq("CREDIT_CARD"), any(BigDecimal.class));
        verify(notificationService).sendOrderConfirmation(result);
    }

    @Test
    @DisplayName("库存不足时抛出异常")
    void testCreateOrder_InsufficientStock() {
        // Given
        when(inventoryService.checkStock(1L, 2)).thenReturn(false);

        // When & Then
        assertThrows(InsufficientStockException.class, () -> {
            orderService.createOrder(orderRequest);
        });

        // 验证不会调用后续方法
        verify(inventoryService).checkStock(1L, 2);
        verify(inventoryService, never()).decreaseStock(anyLong(), anyInt());
        verify(orderRepository, never()).save(any(Order.class));
        verify(paymentService, never()).processPayment(anyLong(), anyString(), any(BigDecimal.class));
    }

    @Test
    @DisplayName("支付失败时回滚库存")
    void testCreateOrder_PaymentFailed() {
        // Given
        when(inventoryService.checkStock(1L, 2)).thenReturn(true);

        Order mockOrder = new Order();
        mockOrder.setId(1L);
        when(orderRepository.save(any(Order.class))).thenReturn(mockOrder);

        PaymentResult paymentResult = new PaymentResult(false, "INSUFFICIENT_FUNDS");
        when(paymentService.processPayment(anyLong(), anyString(), any(BigDecimal.class)))
            .thenReturn(paymentResult);

        // When
        Order result = orderService.createOrder(orderRequest);

        // Then
        assertEquals(OrderStatus.FAILED, result.getStatus());

        // 验证回滚库存
        verify(inventoryService).decreaseStock(1L, 2);
        verify(inventoryService).increaseStock(1L, 2);
        verify(notificationService, never()).sendOrderConfirmation(any(Order.class));
    }
}

7.3 Controller 层测试

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(new OrderResponse(order));
    }
}

// 测试类
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    @DisplayName("POST /api/orders - 成功创建订单")
    void testCreateOrder() throws Exception {
        // Given
        OrderRequest request = new OrderRequest();
        request.setProductId(1L);
        request.setQuantity(2);

        Order mockOrder = new Order();
        mockOrder.setId(1L);
        mockOrder.setStatus(OrderStatus.PAID);

        when(orderService.createOrder(any(OrderRequest.class))).thenReturn(mockOrder);

        // When & Then
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"productId\":1,\"quantity\":2}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.status").value("PAID"));

        verify(orderService).createOrder(any(OrderRequest.class));
    }
}

八、最佳实践

在这里插入图片描述

8.1 优先 Mock 接口而非具体类

// ✅ 推荐:Mock 接口
UserRepository userRepository = mock(UserRepository.class);

// ❌ 不推荐:Mock 具体类(可能遇到 final 方法问题)
UserRepositoryImpl userRepository = mock(UserRepositoryImpl.class);

8.2 避免过度 Mock

// ❌ 过度 Mock - 测试变得脆弱
@Test
void badTest() {
    when(dependency1.method1()).thenReturn("value1");
    when(dependency2.method2()).thenReturn("value2");
    when(dependency3.method3()).thenReturn("value3");
    // ... 大量的 Mock 设置

    // 实际业务逻辑被淹没
    service.doSomething();

    // 验证大量细节
    verify(dependency1, times(2)).method1();
    verify(dependency2).method2();
    // ...
}

// ✅ 适度 Mock - 只关注核心交互
@Test
void goodTest() {
    // 只 Mock 必要的依赖
    when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));

    User result = userService.getUser(1L);

    assertEquals("张三", result.getName());
    verify(userRepository).findById(1L);
}

8.3 使用 @InjectMocks 自动注入

// ✅ 推荐:使用注解自动注入
@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

// ❌ 不推荐:手动创建(容易出错且繁琐)
UserService userService = new UserService(userRepository, emailService, ...);

8.4 合理使用 ArgumentCaptor

// ✅ 推荐:验证复杂对象的属性
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
assertEquals("张三", captor.getValue().getName());

// ❌ 不推荐:简单参数使用 Captor(直接匹配即可)
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(service).process(captor.capture());
assertEquals("value", captor.getValue());  // 直接用 eq("value") 更简洁

8.5 验证重要交互,忽略次要细节

// ✅ 推荐:只验证核心业务逻辑
@Test
void testUserRegistration() {
    userService.register(user);

    verify(userRepository).save(user);  // 核心:保存用户
    verify(emailService).sendWelcome(user.getEmail());  // 核心:发送邮件
    // 不验证日志记录等次要操作
}

// ❌ 不推荐:验证过多细节
verify(logger).info("开始注册用户");
verify(validator).validate(user);
verify(logger).debug("用户验证通过");
// ... 测试变得脆弱

8.6 使用有意义的测试数据

// ✅ 推荐:清晰的测试数据
User testUser = new User("张三", "zhang@example.com", 25);

// ❌ 不推荐:无意义的数据
User testUser = new User("test", "test@test.com", 0);

8.7 一个测试方法只测一个场景

// ✅ 推荐:分离测试场景
@Test
void testCreateUser_Success() {
    // 测试成功场景
}

@Test
void testCreateUser_DuplicateEmail() {
    // 测试邮箱重复场景
}

@Test
void testCreateUser_InvalidInput() {
    // 测试无效输入场景
}

// ❌ 不推荐:在一个测试中测试多个场景
@Test
void testCreateUser() {
    // 成功场景
    // 失败场景
    // 异常场景
    // ... 难以定位问题
}

九、总结

Mockito 是 Java 单元测试的利器,掌握它可以显著提升测试质量和开发效率。 希望这篇文章能帮助你更好地使用 Mockito,写出高质量的单元测试!