Mockito 单测入门
Mockito 单测入门Spring Boot 项目中最精简的 Mockito 示例 — Service / 三方依赖 / Controller1 被测代码准备以下是一个简单的聊天消息服务内含需要测试的三种典型场景。ServicepublicclassChatMsgService{AutowiredprivateChatMsgRepositoryrepo;// Spring 注入AutowiredprivateSmsClientsmsClient;// 三方依赖如阿里云短信 SDKpublicChatMsgsend(Integeruid,Stringmessage){ChatMsgmsgnewChatMsg();msg.setUid(uid);msg.setMessage(message);msg.setCreateTime(LocalDateTime.now());returnrepo.save(msg);// 调用 Spring Bean}publicStringsendVerifyCode(Stringphone){Stringcode123456;smsClient.send(phone,code);// 调用三方 SDKreturncode;}}RestControllerRequestMapping(/chat)publicclassChatMsgController{AutowiredprivateChatMsgServiceservice;PostMapping(/send)publicChatMsgsend(RequestParamIntegeruid,RequestParamStringmessage){returnservice.send(uid,message);}}2 依赖配置dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencyspring-boot-starter-test已包含 Mockito、MockMvc、JUnit 5无需额外引入。3 Service 单测 — InjectMocks Mock核心Mock创建 Mock 对象InjectMocks自动注入到被测类。ExtendWith(MockitoExtension.class)// 启用 MockitoclassChatMsgServiceTest{MockprivateChatMsgRepositoryrepo;// 模拟 Spring BeanMockprivateSmsClientsmsClient;// 模拟三方依赖InjectMocksprivateChatMsgServiceservice;// 自动注入上面两个 mockTestvoidtestSend(){// 准备ChatMsgsavednewChatMsg();saved.setId(1L);saved.setUid(100);saved.setMessage(hello);Mockito.when(repo.save(Mockito.any())).thenReturn(saved);// 执行ChatMsgresultservice.send(100,hello);// 验证Assertions.assertEquals(1L,result.getId());Mockito.verify(repo,Mockito.times(1)).save(Mockito.any());}TestvoidtestSendVerifyCode(){// 执行Stringcodeservice.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;// 模拟 ServiceTestvoidtestSend()throwsException{// 准备ChatMsgmockResultnewChatMsg();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.uiduid;}publicStringgetMessage(){returnmessage;}publicvoidsetMessage(Stringmessage){this.messagemessage;}}TestvoidtestSendJson()throwsException{// 准备ChatMsgmockResultnewChatMsg();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{// 准备ChatMsgmockResultnewChatMsg();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){Stringmd5DigestUtils.md5DigestAsHex(// 静态方法调用message.getBytes());ChatMsgmsgnewChatMsg();msg.setUid(uid);msg.setMessage(message_md5);returnrepo.save(msg);}}TestvoidtestSendWithMd5(){// try-with-resources 包裹超出代码块自动失效try(MockedStaticDigestUtilsmockedMockito.mockStatic(DigestUtils.class)){// 打桩当调用静态方法时返回固定值mocked.when(()-DigestUtils.md5DigestAsHex(Mockito.any())).thenReturn(fake_md5);// 执行ChatMsgresultservice.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(MockedStaticDigestUtilsmockedMockito.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 和第三方 SDKMockInjectMocks三方依赖直接用Mock只验证调用了对应方法MockController 单测MockMvc 模拟 HTTP 请求MockBean 掉 ServiceWebMvcTestMockBean注解启动 Spring替换容器 Bean用在什么测试Mock否否Service 单测不启动 SpringMockBean是是Controller / 集成测试需 Spring 容器InjectMocks否否配合Mock把 mock 塞进手动创建的被测类