Mockito 详解及详细代码展示

Mockito 深度详解与完整代码实战

一、Mockito 核心概念

Mockito 是 Java 最流行的模拟框架,用于单元测试中创建测试替身(Test Doubles)。它通过模拟依赖对象的行为,使单元测试更加隔离可控

核心对象类型:
Mock
模拟对象
Spy
部分真实对象
ArgumentCaptor
参数捕获器
Verification
行为验证

二、环境配置(Maven)

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

三、基础使用:Mock 与 Stubbing

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

// 方式1:静态方法
List<String> mockedList = mock(List.class);

// 方式2:注解(需配合扩展)
@ExtendWith(MockitoExtension.class)
class MockitoTest {
    @Mock
    List<String> mockedList;
}
2. 方法行为设置(Stubbing)
// 基本Stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

// 参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");
when(mockedList.contains(any(String.class))).thenReturn(true);

// 连续Stubbing
when(mockedList.size())
    .thenReturn(1)
    .thenReturn(2)
    .thenThrow(new IllegalStateException());
    
// void方法Stubbing
doThrow(RuntimeException.class).when(mockedList).clear();

四、行为验证

// 基本验证
mockedList.add("one");
verify(mockedList).add("one");

// 验证调用次数
verify(mockedList, times(1)).add("one");
verify(mockedList, never()).add("two");
verify(mockedList, atLeast(2)).size();

// 验证调用顺序
InOrder inOrder = inOrder(mockedList);
inOrder.verify(mockedList).add("one");
inOrder.verify(mockedList).clear();

// 超时验证
verify(mockedList, timeout(100).times(1)).get(0);

五、参数捕获器

@Mock
UserRepository userRepository;

@Captor
ArgumentCaptor<User> userCaptor;

@Test
void testUserRegistration() {
    UserService service = new UserService(userRepository);
    User newUser = new User("john@example.com", "John");
    
    service.registerUser(newUser);
    
    verify(userRepository).save(userCaptor.capture());
    User savedUser = userCaptor.getValue();
    
    assertEquals("John", savedUser.getName());
    assertNotNull(savedUser.getRegistrationDate());
}

六、Spy:部分真实对象

// 创建Spy对象(包装真实实例)
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);

// 默认调用真实方法
spyList.add("real");
spyList.add("data");

// 部分方法模拟
doReturn(100).when(spyList).size();

// 混合行为
assertEquals("real", spyList.get(0)); // 真实方法
assertEquals(100, spyList.size());    // 模拟方法

七、Mockito 与依赖注入

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    PaymentGateway paymentGateway;
    
    @Mock
    InventoryService inventoryService;
    
    @InjectMocks
    OrderService orderService; // 自动注入mocks
    
    @Test
    void placeOrderSuccess() {
        // 设置行为
        when(inventoryService.isInStock("A001")).thenReturn(true);
        when(paymentGateway.process(anyDouble())).thenReturn(true);
        
        // 执行测试
        Order order = new Order("A001", 2, 50.0);
        OrderResult result = orderService.placeOrder(order);
        
        // 验证结果
        assertTrue(result.isSuccess());
        verify(inventoryService).deductStock("A001", 2);
    }
}

八、高级特性

1. 自定义应答(Answer)
when(mockedList.get(anyInt())).thenAnswer(invocation -> {
    int index = invocation.getArgument(0);
    return "Element at " + index;
});

assertEquals("Element at 5", mockedList.get(5));
2. 静态方法模拟(Mockito 4.x+)
try (MockedStatic<Utility> mocked = mockStatic(Utility.class)) {
    mocked.when(Utility::generateId).thenReturn("fixed-id");
    
    assertEquals("fixed-id", Utility.generateId());
    mocked.verify(Utility::generateId);
}
3. BDD 风格(Given-When-Then)
import static org.mockito.BDDMockito.*;

@Test
void bddStyleTest() {
    // Given
    given(userRepository.findById(1L)).willReturn(Optional.of(new User(1L, "Alice")));
    
    // When
    User user = userService.getUserById(1L);
    
    // Then
    then(userRepository).should().findById(1L);
    assertEquals("Alice", user.getName());
}

九、完整业务场景测试示例

业务类
public class PaymentProcessor {
    private final PaymentGateway gateway;
    private final FraudDetectionService fraudDetection;
    
    public PaymentProcessor(PaymentGateway gateway, FraudDetectionService fraudDetection) {
        this.gateway = gateway;
        this.fraudDetection = fraudDetection;
    }
    
    public PaymentResult process(PaymentRequest request) {
        if (fraudDetection.isSuspicious(request)) {
            return PaymentResult.fraudDetected();
        }
        
        boolean success = gateway.processPayment(
            request.getAmount(), 
            request.getCurrency(),
            request.getCardDetails()
        );
        
        return success ? 
            PaymentResult.success() : 
            PaymentResult.failed("Payment failed");
    }
}
测试类
@ExtendWith(MockitoExtension.class)
class PaymentProcessorTest {
    
    @Mock PaymentGateway paymentGateway;
    @Mock FraudDetectionService fraudDetection;
    
    @InjectMocks PaymentProcessor processor;
    
    @Test
    void processPaymentSuccess() {
        // Given
        PaymentRequest request = new PaymentRequest(100.0, "USD", "4111111111111111");
        given(fraudDetection.isSuspicious(request)).willReturn(false);
        given(paymentGateway.processPayment(100.0, "USD", "4111111111111111"))
            .willReturn(true);
        
        // When
        PaymentResult result = processor.process(request);
        
        // Then
        assertTrue(result.isSuccess());
        then(paymentGateway).should().processPayment(100.0, "USD", "4111111111111111");
    }
    
    @Test
    void fraudDetectionBlocksPayment() {
        // Given
        PaymentRequest request = new PaymentRequest(5000.0, "USD", "1234567890123456");
        given(fraudDetection.isSuspicious(request)).willReturn(true);
        
        // When
        PaymentResult result = processor.process(request);
        
        // Then
        assertTrue(result.isFraud());
        then(paymentGateway).should(never()).processPayment(anyDouble(), anyString(), anyString());
    }
    
    @Test
    void paymentGatewayFailure() {
        // Given
        PaymentRequest request = new PaymentRequest(50.0, "EUR", "5555555555554444");
        given(fraudDetection.isSuspicious(request)).willReturn(false);
        given(paymentGateway.processPayment(50.0, "EUR", "5555555555554444"))
            .willReturn(false);
        
        // When
        PaymentResult result = processor.process(request);
        
        // Then
        assertFalse(result.isSuccess());
        assertEquals("Payment failed", result.getErrorMessage());
    }
}

十、Mockito 最佳实践

1. 测试结构(AAA 模式)
@Test
void testStructure() {
    // Arrange: 准备测试数据和行为
    when(repository.findByName("test")).thenReturn(new Entity());
    
    // Act: 执行被测方法
    Result result = service.execute("test");
    
    // Assert: 验证结果和行为
    assertNotNull(result);
    verify(repository).findByName("test");
}
2. 避免过度使用 Mocks
  • 优先测试真实对象:对值对象、工具类等使用真实实例
  • 不要 Mock 不可控类型:如集合类、日期类
  • 避免 Mock 第三方库:使用测试替身或容器化测试
3. 合理使用 ArgumentMatchers
// 精确匹配
verify(repo).findById(eq(1L));

// 灵活匹配
verify(repo).save(any(User.class));
verify(repo).delete(argThat(user -> user.getName().startsWith("A")));
4. 测试异常场景
@Test
void testException() {
    // 设置异常行为
    when(service.dangerousOperation()).thenThrow(new IllegalStateException());
    
    // 验证异常
    assertThrows(IllegalStateException.class, () -> {
        controller.handleRequest();
    });
    
    // 验证异常后的清理操作
    verify(cleanupService).execute();
}

十一、常见问题解决方案

问题解决方案
NPE 未初始化 Mock添加 @ExtendWith(MockitoExtension.class)
when() 调用真实方法使用 doReturn().when() 替代
泛型类型警告使用 @Mock 代替 mock() 或添加 @SuppressWarnings
静态方法模拟失败确保使用 MockedStatic 和 try-with-resources
验证参数为 null使用 isNull() 匹配器
多次调用返回相同值使用 thenReturn 链式调用

十二、Mockito 架构原理

测试代码
Mockito Core
Mock Creation
Stubbing Engine
Invocation Handler
行为验证
自定义应答
调用记录

十三、Mockito 与 Spring Boot 集成

@SpringBootTest
class IntegrationTest {
    
    @MockBean
    private ExternalService externalService; // Spring管理的Mock
    
    @Autowired
    private MainService mainService;
    
    @Test
    void testIntegration() {
        given(externalService.callApi()).willReturn("mocked");
        
        String result = mainService.execute();
        
        assertEquals("processed: mocked", result);
    }
}

十四、高级场景:深度模拟

@Test
void testDeepMock() {
    OrderService orderService = mock(OrderService.class);
    Order order = mock(Order.class);
    
    // 深度Stubbing
    when(orderService.getOrder(anyLong())).thenReturn(order);
    when(order.getItems()).thenReturn(Arrays.asList("Item1", "Item2"));
    
    // 执行测试
    Order retrieved = orderService.getOrder(1L);
    List<String> items = retrieved.getItems();
    
    // 验证
    assertEquals(2, items.size());
    verify(order).getItems();
}

十五、Mockito 性能优化

  1. 重用 Mock 对象:在 @BeforeEach 中初始化
  2. 轻量级测试:避免复杂逻辑在 Stubbing 中
  3. 限制 Mock 数量:每个测试不超过 3-5 个 Mock
  4. 使用 @Mock 替代 mock():减少样板代码

Mockito 核心价值

  1. 测试隔离:切断外部依赖,专注单元逻辑
  2. 行为验证:确保对象间正确交互
  3. 异常模拟:轻松测试错误处理路径
  4. 快速反馈:无需启动整个应用上下文

最佳实践组合

  • 单元测试:Mockito + JUnit5
  • 集成测试:Mockito + SpringBootTest + Testcontainers
  • 行为验证:Mockito + AssertJ
  • 覆盖率:Mockito + JaCoCo

完整项目结构示例:

src/
├── main/
│   └── java/
│       └── com/
│           └── example/
│               ├── service/
│               ├── repository/
│               └── model/
└── test/
    └── java/
        └── com/
            └── example/
                ├── unit/         # 纯Mockito测试
                ├── integration/  # 集成测试
                └── e2e/          # 端到端测试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值