Mockito 单测入门
Mockito 单测入门
Spring Boot 项目中最精简的 Mockito 示例 — Service / 三方依赖 / Controller
1 被测代码准备
以下是一个简单的聊天消息服务,内含需要测试的三种典型场景。
@ServicepublicclassChatMsgService{@AutowiredprivateChatMsgRepositoryrepo;// Spring 注入@AutowiredprivateSmsClientsmsClient;// 三方依赖(如阿里云短信 SDK)publicChatMsgsend(Integeruid,Stringmessage){ChatMsgmsg=newChatMsg();msg.setUid(uid);msg.setMessage(message);msg.setCreateTime(LocalDateTime.now());returnrepo.save(msg);// 调用 Spring Bean}publicStringsendVerifyCode(Stringphone){Stringcode="123456";smsClient.send(phone,code);// 调用三方 SDKreturncode;}}@RestController@RequestMapping("/chat")publicclassChatMsgController{@AutowiredprivateChatMsgServiceservice;@PostMapping("/send")publicChatMsgsend(@RequestParamIntegeruid,@RequestParamStringmessage){returnservice.send(uid,message);}}2 依赖配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>spring-boot-starter-test已包含 Mockito、MockMvc、JUnit 5,无需额外引入。
3 Service 单测 — @InjectMocks + @Mock
核心:@Mock创建 Mock 对象,@InjectMocks自动注入到被测类。
@ExtendWith(MockitoExtension.class)// 启用 MockitoclassChatMsgServiceTest{@MockprivateChatMsgRepositoryrepo;// 模拟 Spring Bean@MockprivateSmsClientsmsClient;// 模拟三方依赖@InjectMocksprivateChatMsgServiceservice;// 自动注入上面两个 mock@TestvoidtestSend(){// 准备ChatMsgsaved=newChatMsg();saved.setId(1L);saved.setUid(100);saved.setMessage("hello");Mockito.when(repo.save(Mockito.any())).thenReturn(saved);// 执行ChatMsgresult=service.send(100,"hello");// 验证Assertions.assertEquals(1L,result.getId());Mockito.verify(repo,Mockito.times(1)).save(Mockito.any());}@TestvoidtestSendVerifyCode(){// 执行Stringcode=service.sendVerifyCode("13800138000");// 验证 — 不关心三方 SDK 内部实现,只验证它被调用了Assertions.assertEquals("123456",code);Mockito.verify(smsClient,Mockito.times(1)).send("13800138000","123456");}}为什么三方依赖这么测?短信 SDK 真实发送会扣费且依赖网络。
用@Mock让它"什么都不做",我们只验证 service 正确调用了它并返回了预期结果。
4 Controller 单测 — @WebMvcTest + MockMvc
只加载 Web 层,Service 用@MockBean注入 Mock。
@WebMvcTest(ChatMsgController.class)// 只启动 Controller 层classChatMsgControllerTest{@AutowiredprivateMockMvcmockMvc;// HTTP 模拟客户端@MockBeanprivateChatMsgServiceservice;// 模拟 Service@TestvoidtestSend()throwsException{// 准备ChatMsgmockResult=newChatMsg();mockResult.setId(1L);mockResult.setUid(100);mockResult.setMessage("hello");Mockito.when(service.send(100,"hello")).thenReturn(mockResult);// 执行 & 验证 — 模拟 HTTP 请求,断言响应mockMvc.perform(MockMvcRequestBuilders.post("/chat/send").param("uid","100").param("message","hello")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)).andExpect(MockMvcResultMatchers.jsonPath("$.message").value("hello"));}}5 Controller POST JSON 参数测试
当接口接收 JSON 请求体时,使用@RequestBody接收参数,测试时通过.contentType(MediaType.APPLICATION_JSON)+.content()传入 JSON 字符串。
@PostMapping("/sendJson")publicChatMsgsendJson(@RequestBodyChatMsgRequestreq){returnservice.send(req.getUid(),req.getMessage());}// 配合的 DTOpublicclassChatMsgRequest{privateIntegeruid;privateStringmessage;publicIntegergetUid(){returnuid;}publicvoidsetUid(Integeruid){this.uid=uid;}publicStringgetMessage(){returnmessage;}publicvoidsetMessage(Stringmessage){this.message=message;}}@TestvoidtestSendJson()throwsException{// 准备ChatMsgmockResult=newChatMsg();mockResult.setId(1L);mockResult.setUid(100);mockResult.setMessage("hello");Mockito.when(service.send(100,"hello")).thenReturn(mockResult);// JSON 请求体StringjsonBody="{\"uid\":100,\"message\":\"hello\"}";// 执行 & 验证mockMvc.perform(MockMvcRequestBuilders.post("/chat/sendJson").contentType(MediaType.APPLICATION_JSON).content(jsonBody)).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));}关键点:
.contentType(MediaType.APPLICATION_JSON)告诉 Spring 请求体是 JSON 格式;.content(jsonBody)传入 JSON 字符串。注意 JSON 中的引号需要转义。
6 Controller Header 传参测试
接口需要从 Header 中获取参数(如 token、traceId)时,使用@RequestHeader注入,测试时通过.header("key", "value")传入。
@PostMapping("/sendWithToken")publicChatMsgsendWithToken(@RequestHeader("token")Stringtoken,@RequestParamIntegeruid,@RequestParamStringmessage){// token 可用于鉴权,这里省略校验逻辑returnservice.send(uid,message);}@TestvoidtestSendWithToken()throwsException{// 准备ChatMsgmockResult=newChatMsg();mockResult.setId(1L);mockResult.setUid(100);mockResult.setMessage("hello");Mockito.when(service.send(100,"hello")).thenReturn(mockResult);// 执行 & 验证 — 通过 .header() 传入请求头mockMvc.perform(MockMvcRequestBuilders.post("/chat/sendWithToken").header("token","abc123").param("uid","100").param("message","hello")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));}关键点:
.header("token", "abc123")模拟 HTTP 请求头。可调用多次传入多个 header。
如果有多个同名 header 需要传多个值,使用.header("key", "value1", "value2")或.header("key", new String[]{"v1","v2"})。
7 常用 Mockito API 速查
// ---- 打桩 ----when(foo.bar()).thenReturn(xxx);// 返回固定值when(foo.bar()).thenThrow(newRuntimeException());// 抛异常when(foo.bar(anyInt())).thenAnswer(inv->42);// 动态返回// ---- 验证 ----verify(foo).bar();// 是否调用过verify(foo,times(2)).bar();// 调用次数verify(foo,never()).bar();// 从未调用verifyNoInteractions(foo);// 无任何交互// ---- 匹配器 ----any()anyInt()anyString()anyList()anyLong()anyBoolean()any(LocalDateTime.class)8 静态方法 Mock — mockStatic
静态方法(如工具类)用Mockito.mockStatic(),需要在 try-with-resources 块内使用,mock 作用域仅限该块。
@ServicepublicclassChatMsgService{publicChatMsgsendWithMd5(Integeruid,Stringmessage){Stringmd5=DigestUtils.md5DigestAsHex(// 静态方法调用message.getBytes());ChatMsgmsg=newChatMsg();msg.setUid(uid);msg.setMessage(message+"_"+md5);returnrepo.save(msg);}}@TestvoidtestSendWithMd5(){// try-with-resources 包裹,超出代码块自动失效try(MockedStatic<DigestUtils>mocked=Mockito.mockStatic(DigestUtils.class)){// 打桩:当调用静态方法时返回固定值mocked.when(()->DigestUtils.md5DigestAsHex(Mockito.any())).thenReturn("fake_md5");// 执行ChatMsgresult=service.sendWithMd5(100,"hello");// 验证静态方法被调用mocked.verify(()->DigestUtils.md5DigestAsHex(Mockito.any()),Mockito.times(1));Assertions.assertTrue(result.getMessage().contains("fake_md5"));}// 退出 try 块后,静态 mock 自动失效,不影响其他测试}// ====== 异常模拟 ======@TestvoidtestRepoThrows(){// 模拟 Spring Bean 抛异常Mockito.when(repo.save(Mockito.any())).thenThrow(newRuntimeException("DB down"));Assertions.assertThrows(RuntimeException.class,()->service.send(100,"hello"));}@TestvoidtestStaticMethodThrows(){try(MockedStatic<DigestUtils>mocked=Mockito.mockStatic(DigestUtils.class)){// 模拟静态方法抛异常mocked.when(()->DigestUtils.md5DigestAsHex(Mockito.any())).thenThrow(newIllegalArgumentException("bad input"));Assertions.assertThrows(IllegalArgumentException.class,()->service.sendWithMd5(100,"hello"));}}注意:Mockito 静态 mock 需要 Mockito 3.4.0+ 和
mockito-inline依赖。
Spring Boot 2.5+ / 3.x 的spring-boot-starter-test默认已包含,无需额外配置。
9 一句话总结
| 场景 | 做法 | 注解 |
|---|---|---|
| Service 单测 | Mock 掉 Repository 和第三方 SDK | @Mock+@InjectMocks |
| 三方依赖 | 直接用@Mock,只验证调用了对应方法 | @Mock |
| Controller 单测 | MockMvc 模拟 HTTP 请求,MockBean 掉 Service | @WebMvcTest+@MockBean |
| 注解 | 启动 Spring? | 替换容器 Bean? | 用在什么测试 |
|---|---|---|---|
@Mock | 否 | 否 | Service 单测(不启动 Spring) |
@MockBean | 是 | 是 | Controller / 集成测试(需 Spring 容器) |
@InjectMocks | 否 | 否 | 配合@Mock把 mock 塞进手动创建的被测类 |
