文档站点
Skip to content

模拟对象(Mockito) ​

Laravel 用 Mockery:Mockery::mock(...)、shouldReceive;Java 用 Mockito:mock(...)、when(...).thenReturn(...)、verify(...)。思想完全一样:造一个「假的依赖」替身,专注测被测类的逻辑。

为什么需要 Mock ​

测 PostService.create() 时,它依赖 PostRepository(要连数据库)。不 mock 的话测试就得真连库。用 mock 替身:不用真数据库,还能断言「Service 是否正确地调用了 Repository」。

基本用法 ​

java
package com.example.blog.post;

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 static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class PostServiceTest {

    @Mock
    private PostRepository postRepository;          // 假的 Repository

    @InjectMocks
    private PostService postService;                // 被测对象,Mock 自动注入

    @Test
    void 创建帖子调用save() {
        Post post = new Post();
        post.setTitle("标题");

        when(postRepository.save(any(Post.class))).thenReturn(post);

        Post result = postService.create(post);
        assertEquals("标题", result.getTitle());
        verify(postRepository, times(1)).save(any(Post.class));   // 断言被调用一次
    }
}

对照 Mockery

Mockery(Laravel)Mockito(Java)
$repo = Mockery::mock(PostRepository::class)@Mock PostRepository postRepository
$repo->shouldReceive('save')->andReturn($post)when(postRepository.save(...)).thenReturn(post)
$repo->shouldHaveReceived('save')verify(postRepository).save(...)
注入到被测类@InjectMocks

常用 mock 行为 ​

java
// 返回固定值
when(repo.findById(1L)).thenReturn(Optional.of(post));

// 抛异常
when(repo.findById(999L)).thenThrow(BusinessException.notFound("不存在"));

// 匹配任意参数
when(repo.save(any(Post.class))).thenReturn(post);
when(repo.findByStatus(anyInt())).thenReturn(List.of(post));

// 无返回值方法
doNothing().when(notifier).send(anyString());
doThrow(new RuntimeException()).when(cache).evict(anyString());

// 连续调用返回不同值
when(repo.findById(1L))
        .thenReturn(Optional.of(post1))
        .thenThrow(new RuntimeException("第二次调用抛异常"));

常用验证(verify) ​

java
verify(repo).save(post);                          // 调用过一次
verify(repo, times(2)).save(post);                // 调用过两次
verify(repo, never()).delete(any(Post.class));    // 从未调用
verify(repo, atLeastOnce()).findById(1L);         // 至少一次
verify(repo, timeout(1000)).save(post);           // 超时内被调用(异步)

综合实战:Service 测试 ​

java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;
    @Mock
    private InventoryClient inventoryClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    void 下单成功() {
        when(inventoryClient.isAvailable(1L, 10))
                .thenReturn(true);
        when(orderRepository.save(any(Order.class)))
                .thenAnswer(inv -> {                 // thenAnswer 可以拿到参数处理
                    Order o = inv.getArgument(0);
                    o.setId(100L);
                    return o;
                });

        Order order = orderService.placeOrder(1L, 10);

        assertNotNull(order.getId());
        verify(inventoryClient).isAvailable(1L, 10);
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void 库存不足抛异常() {
        when(inventoryClient.isAvailable(1L, 10)).thenReturn(false);

        assertThrows(BusinessException.class,
                () -> orderService.placeOrder(1L, 10));
        verify(orderRepository, never()).save(any(Order.class));   // 没走到保存
    }
}

TIP

thenAnswer 比 thenReturn 更强大:可以在 mock 方法里拿到入参、动态生成返回值(上面的回填 id)。测试业务逻辑的核心套路就这三个:

  1. when...thenReturn:安排依赖行为
  2. assertXxx:断言结果
  3. verify:断言依赖被正确调用

Mock 掉被 @Async 的依赖 ​

测试异步方法时,直接调方法结果不好断言。用 CompletableFuture + thenAnswer:

java
when(notifier.sendAsync(anyString()))
        .thenReturn(CompletableFuture.completedFuture("ok"));

service.doSomething();
verify(notifier).sendAsync("消息");     // 只验证"发出去了"

常见坑 ​

坑说明
when(...) 忘写调用参数精确匹配 vs any() 混淆,导致不匹配
Mock 了 final 类/方法需要 mockito-inline,或重构代码别用 final
verify 忘了写没断言「依赖被调用了」,测试意义减半
Mockito 和 Spring 混用单测用 Mockito,集成测试用 @MockBean,别混
同一方法多次调用忘了 thenAnswer第二次返回 null,NPE

在 Spring 集成测试里 mock:@MockBean ​

@WebMvcTest / @SpringBootTest 里想替换真实 Bean 用 @MockBean(与纯 Mockito 不同,它替换的是容器里的 Bean):

java
@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PostService postService;      // 容器里这个 Bean 被替换成 mock

    @Test
    void 列表接口() throws Exception {
        when(postService.list()).thenReturn(List.of(new Post()));
        mockMvc.perform(get("/api/posts")).andExpect(status().isOk());
    }
}

TIP

纯单元测试(不启动 Spring)用 @Mock + @InjectMocks + @ExtendWith(MockitoExtension.class);需要启动 Spring 的测试用 @MockBean。前者快、后者全,按需选。

对照速查 ​

需求MockeryMockito
造替身Mockery::mock@Mock / mock(...)
注入替身Mockery::mock(...) 手动传@InjectMocks
安排行为shouldReceive->andReturnwhen(...).thenReturn(...)
断言调用shouldHaveReceivedverify(...)
断言调用次数->twice()verify(..., times(2))
抛异常->andThrowwhen(...).thenThrow(...)

面向 PHP / Laravel 开发者的 Spring Boot 中文文档