模拟对象(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)。测试业务逻辑的核心套路就这三个:
- when...thenReturn:安排依赖行为
- assertXxx:断言结果
- 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。前者快、后者全,按需选。
对照速查
| 需求 | Mockery | Mockito |
|---|---|---|
| 造替身 | Mockery::mock | @Mock / mock(...) |
| 注入替身 | Mockery::mock(...) 手动传 | @InjectMocks |
| 安排行为 | shouldReceive->andReturn | when(...).thenReturn(...) |
| 断言调用 | shouldHaveReceived | verify(...) |
| 断言调用次数 | ->twice() | verify(..., times(2)) |
| 抛异常 | ->andThrow | when(...).thenThrow(...) |