Junit到Springboot单元测试
第一部分 junit与springboot的前世今生一、junit4与junit5及springboot中的使用在现代软件开发中单元测试是确保代码质量的重要环节。Spring Boot框架通过整合JUnit为开发者提供了便捷的单元测试支持。1.1 Spring Boot中JUnit版本的变化在Spring Boot 2.0之前框架默认使用JUnit 4作为测试平台。然而从Spring Boot 2.0开始JUnit 5成为默认的测试框架。以下是Spring Boot不同版本中JUnit版本的对比Spring Boot版本默认JUnit版本1.xJUnit 42.xJUnit 5例如Spring Boot 2.2.0使用JUnit 5.5.2版本。开发者可以通过POM文件确认具体版本。1.2 POM文件配置在Spring Boot项目中单元测试的依赖通过spring-boot-starter-test启动器引入。以下是POM文件的配置示例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope exclusions exclusion groupIdorg.junit.vintage/groupId artifactIdjunit-vintage-engine/artifactId /exclusion /exclusions /dependency1.3 排除JUnit Vintage Enginejunit-vintage-engine是JUnit 3和JUnit 4的运行支持平台。默认情况下Spring Boot测试启动器会排除该依赖以鼓励开发者使用JUnit 5。如果需要使用JUnit 4可以移除exclusions标签。二、JUnit 4与JUnit 5的对比以下是JUnit 4和JUnit 5的主要差异特性JUnit 4JUnit 5注解RunWith、TestSpringBootTest、Test默认启动类支持需要手动指定启动类自动检测启动类测试方法支持需要Test注解需要Test注解扩展支持有限更强大的扩展机制三、JUnit 4测试代码示例以下是一个基于JUnit 4的测试代码示例import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.junit.Test; RunWith(SpringJUnit4ClassRunner.class) SpringBootTest(classes SpringBootExceptionAndJourneyApplication.class) public class UserServiceTest { Test public void testAddUser() { System.out.println(JUnit 4 测试方法运行成功); } }四、JUnit 5测试代码示例以下是一个基于JUnit 5的测试代码示例import org.springframework.boot.test.context.SpringBootTest; import org.junit.jupiter.api.Test; SpringBootTest public class UserServiceTest { Test public void testAddUser() { System.out.println(JUnit 5 测试方法运行成功); } }五、实际案例持久层与业务层测试假设我们有一个UserDAO和UserService以下是它们的实现代码5.1 持久层代码public class UserDAOImpl { public void insert() { System.out.println(INSERT INTO USER VALUES(...)); } }5.2 业务层代码import org.springframework.stereotype.Service; Service public class UserServiceImpl { private final UserDAOImpl userDAO; public UserServiceImpl(UserDAOImpl userDAO) { this.userDAO userDAO; } public void addUser() { userDAO.insert(); } }5.3 测试代码import org.springframework.boot.test.context.SpringBootTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; SpringBootTest public class UserServiceTest { Autowired private UserServiceImpl userService; Test public void testAddUser() { userService.addUser(); } }六、常见问题与解答以下是关于Spring Boot整合JUnit单元测试的常见问题问题答案1. Spring Boot默认使用哪个JUnit版本Spring Boot 2.x默认使用JUnit 5。2. 如何在Spring Boot中使用JUnit 4需要移除POM文件中junit-vintage-engine的排除配置。3. JUnit 5是否需要指定启动类默认情况下JUnit 5会自动检测启动类。如果项目中有多个启动类需要通过SpringBootTest(classes ...)指定。4. JUnit 5的测试方法是否需要RunWith注解不需要JUnit 5使用SpringBootTest注解即可。5. 如何确认JUnit版本通过POM文件中的依赖版本确认例如junit-jupiter-api的版本号。七、流程图Spring Boot整合JUnit单元测试的步骤创建Spring Boot项目添加spring-boot-starter-test依赖确认JUnit版本编写测试代码运行测试第一部分总结我们现在直接使用junit5在springboot项目中使用即可。使用方式为添加注解SpringBootTest(会自己找启动类有多个则通过classes ...指定)就可以让容器启动来测试。疑问1不是说5的话boot项目需要ext注解吗AI解答ExtendWith确实是 JUnit 5 的核心注解。不过在 Spring Boot 项目里你通常不需要手动添加它。 为什么会有这个误解你的理解是对的JUnit 5 确实使用ExtendWith来集成 Spring 的功能它取代了 JUnit 4 的RunWith。在早期版本的 Spring Boot (2.1.x 之前)确实需要手动在测试类上写上ExtendWith(SpringExtension.class)。✅ 现在的实际情况是怎样的从Spring Boot 2.1.x版本开始官方为了让开发更便捷做了一个“贴心”的优化。SpringBootTest这个我们最常用的注解其内部源码已经自动包含了ExtendWith(SpringExtension.class)。你可以把它理解为SpringBootTest是一个“大礼包”里面已经帮你把ExtendWith这个“工具”放进去了。所以当你使用SpringBootTest时就无需再重复添加了。代码对比早期写法 (已过时)// 在 Spring Boot 2.1 之前需要这样写 ExtendWith(SpringExtension.class) // 手动启用 Spring 扩展 SpringBootTest public class OrderServiceTest { // ... }现在写法 (推荐)// 现在这样写就足够了 SpringBootTest public class OrderServiceTest { // ... } 什么时候才需要手动写ExtendWith只有在一些不使用SpringBootTest的底层测试场景中你才可能需要手动添加。例如如果你只用ContextConfiguration来加载一个特定的 Spring 配置文件进行测试这时就需要手动通过ExtendWith(SpringExtension.class)来启用 Spring 的扩展支持。但对于绝大多数 Spring Boot 的集成测试来说放心地只使用SpringBootTest吧疑问2有必要每次启动整个容器吗请看第二部分第二部分 springboot项目怎么进行mock很多开发者在刚开始写测试时都会纠结是“启动整个 Spring 容器”还是“只测一个类”。简单来说“优雅”的测试 选对测试类型 用好 Mock 隔离 一、Mock 是啥解决啥问题1. 通俗解释什么是 Mock想象一下你是一名汽车引擎设计师你在测试UserService你需要测试引擎UserService的运转是否良好。如果不使用 Mock你必须把引擎装到整辆车里连上油箱、排气管、轮胎甚至要把车开到路上启动 Spring 容器、连接真实数据库、连接真实 Redis。这非常慢而且如果车打不着火你不知道是引擎坏了还是油箱漏了还是轮胎没气。使用 Mock你在实验室里给引擎接上一个模拟油箱Mock Repository和一个模拟排气管Mock EmailService。你可以控制“模拟油箱”里有多少油Stubbing预设返回值。你可以观察引擎是否真的向“模拟排气管”排气了Verification验证调用。重点你只测试引擎本身不关心外面的世界。2. Mock 解决了什么问题速度极快不需要启动 Spring 容器不需要连接数据库IO 操作最耗时。单元测试通常是毫秒级的。隔离性强如果测试失败了肯定是你的Service逻辑写错了而不是因为数据库连不上或者网络波动。覆盖极端情况你可以轻松模拟“数据库挂了”或者“查不到数据”的场景而在真实环境中很难故意制造这些故障。✨ 二、如何“优雅”地执行 Boot 项目单元测试在 Spring Boot 中优雅的核心在于“各司其职”。不要把所有测试都写成SpringBootTest启动全容器那样太慢了。我们需要区分两种测试策略1. 纯单元测试 (Unit Test) —— 推荐用于 Service 层特点完全不启动 Spring 容器纯 Java 代码运行。工具JUnit 5 Mockito (Mock,InjectMocks)。场景测试UserService里的业务逻辑比如计算价格、校验参数。代码示例// 1. 不需要 SpringBootTest不需要启动容器 // 使用 Mockito 的扩展来初始化 Mock 对象 ExtendWith(MockitoExtension.class) class UserServiceTest { // 2. Mock: 创建一个假的 UserRepository它是空的需要你喂数据 Mock private UserRepository userRepository; // 3. InjectMocks: 创建 UserService 实例并把上面的 userRepository 塞进去 InjectMocks private UserService userService; Test void shouldFindUserById() { // --- Arrange (准备) --- User mockUser new User(1L, Alice); // 告诉 Mock 对象当有人调用 findById(1L) 时返回 mockUser when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser)); // --- Act (执行) --- User result userService.findById(1L); // --- Assert (断言) --- assertThat(result.getName()).isEqualTo(Alice); // --- Verify (验证) --- // 验证 userRepository.findById 是否真的被调用了一次 verify(userRepository, times(1)).findById(1L); } }优雅点速度飞快完全隔离。2. 切片测试 / 集成测试 (Slice Test) —— 推荐用于 Controller 或 Repository特点只启动 Spring 容器的一部分比如只启动 Web 层或者只启动 JPA 层。工具WebMvcTest(控制器),DataJpaTest(数据库),MockBean。场景测试UserController的接口映射是否正确或者测试 SQL 语句是否正确。代码示例 (测试 Controller)// 1. WebMvcTest: 只启动 Web 层相关的 Bean (Controller, Converter 等)不启动 Service WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; // Spring 提供的模拟 HTTP 客户端 // 2. MockBean: 这是 Spring 的注解 // 它会去 Spring 容器里把 UserService 替换成一个 Mock 对象 MockBean private UserService userService; Test void shouldReturnUserJson() throws Exception { // --- Arrange --- // 模拟 Service 层返回数据 when(userService.findById(1L)).thenReturn(new User(1L, Alice)); // --- Act Assert --- // 发送一个模拟的 GET 请求 mockMvc.perform(get(/users/1)) .andExpect(status().isOk()) // 期望状态码 200 .andExpect(jsonPath($.name).value(Alice)); // 期望返回 JSON 中有 name: Alice } }优雅点比SpringBootTest快但又能测试 Spring 的注解如RestController,RequestMapping是否生效。 三、总结Mock 注解对比表这是最容易混淆的地方请注意区分表格特性Mock(Mockito)MockBean(Spring Boot)所属库MockitoSpring Boot Test是否启动 Spring否(纯单元测试)是(集成测试/切片测试)作用范围仅在测试类内部有效会替换 Spring 容器中的 Bean使用场景测试 Service 业务逻辑测试 Controller, 或者需要 Spring 注入的场景性能极快 (毫秒级)较快 (秒级取决于加载多少组件) 四、最佳实践建议Service 层优先使用ExtendWith(MockitoExtension.class)Mock。不要动不动就SpringBootTest那样太慢了。Controller 层使用WebMvcTestMockBean。Repository 层使用DataJpaTest(它会自动配置内存数据库 H2)。全链路测试只有当你需要测试“整个应用能不能跑起来”或者“配置类是否正确”时才使用SpringBootTest。这样分层测试你的项目构建速度会非常快而且逻辑清晰维护起来也很优雅。第二部分总结使用mock可以最小化范围测试而不是启动整个容器。一般测试的都是service层直接使用ExtendWith(MockitoExtension.class)Mock。不要动不动就SpringBootTest那样太慢了第三部分 Mock中的常见问题一、mock原理就是 “伪造” 依赖接口 / 对象 / 函数的返回结果让程序在没有真实后端、真实服务时也能正常跑、正常测。原理为通过动态代理、字节码增强或请求拦截等方式劫持目标方法 / 接口调用跳过真实逻辑执行并直接返回预设伪造数据从而实现依赖隔离与行为模拟。✨ 疑问final类怎么模拟呢在 Java 中final 关键字的设计初衷就是为了防止继承类或重写方法。而 Mockito 的核心原理恰恰是生成子类动态代理来拦截方法调用。 所以默认情况下Mockito 无法 Mock final 类或 final 方法。如果你强行去 mock通常会报 Cannot mock/spy class ... final class 的错误。 方案使用mockito-inline推荐现代做法这是目前最主流的做法。从 Mockito 2.x 后期版本开始官方提供了一个扩展模块mockito-inline它利用 Java Instrumentation API 在运行时修改字节码从而支持 Mockfinal类。适用场景Spring Boot 2.x (较新版本) 或 Spring Boot 3.x且你不想引入沉重的 PowerMock。1. 添加依赖虽然 Spring Boot 的spring-boot-starter-test已经包含了mockito-core但你需要额外引入mockito-inline。dependency groupIdorg.mockito/groupId artifactIdmockito-inline/artifactId version5.x.x/version !-- 版本号通常与 mockito-core 保持一致 -- scopetest/scope /dependency2. 开启配置关键步骤仅仅加依赖是不够的你必须告诉 Mockito 使用这个“内联”模式。在src/test/resources目录下创建一个文件夹mockito-extensions并在其中创建一个文件org.mockito.plugins.MockMaker。文件路径src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker文件内容mock-makerinline3. 编写测试配置好后你就可以像 Mock 普通类一样 Mockfinal类了代码完全不用变// 假设 FinalService 是一个 final 类 final class FinalService { public String sayHello() { return Hello; } } ExtendWith(MockitoExtension.class) class FinalServiceTest { Mock private FinalService finalService; // 直接 Mock不会报错 Test void testFinalClass() { when(finalService.sayHello()).thenReturn(Mocked Hello); assertEquals(Mocked Hello, finalService.sayHello()); } }二、 Spy 是啥解决什么问题1. 核心概念部分模拟Mock完全模拟创建一个空壳对象。所有方法默认都不执行真实代码直接返回 null 或 0。你必须手动定义每一个方法的行为。Spy部分模拟包装一个真实的对象。默认情况下它会执行真实的代码。只有当你明确告诉它“这个方法要拦截”时它才会返回假数据。2. 解决什么问题场景一遗留代码或复杂对象。当你有一个类方法很多你只想 Mock 其中一个很难测的方法比如调用了外部 API而其他方法逻辑很复杂你不想重写这时用 Spy 最省事。场景二验证真实调用。你想确保某个方法被调用了同时还想验证它执行后的真实副作用。️ 怎么用核心语法在 Spring Boot 项目中我们通常分两种情况使用 Spy纯单元测试和Spring 容器集成测试。1. 纯单元测试使用Spy这是 Mockito 的原生用法用于测试普通的 Java 类。关键点使用 Spy 时存根语法Stubbing必须换Mock 用when(mock.method()).thenReturn(...)Spy 用doReturn(...).when(spy).method()为什么因为 Spy 默认执行真实方法如果用when(spy.method())真实方法会立即执行可能导致空指针异常。ExtendWith(MockitoExtension.class) class UserServiceTest { // 1. 必须初始化真实对象不能写 Spy private UserService userService; (这样会报空指针) Spy private UserService userService new UserService(); Mock private UserRepository userRepository; Test void testSpyUsage() { // --- Arrange --- // 假设 UserService 有个方法 calculateTax() 很复杂我们想 Mock 它 // 注意语法doReturn(...).when(spy).method() doReturn(100.0).when(userService).calculateTax(); // --- Act --- // 调用其他未 Mock 的方法会执行真实逻辑 // 调用 calculateTax会返回 100.0 double tax userService.calculateTax(); // --- Assert --- assertEquals(100.0, tax); // --- Verify --- // 验证真实方法是否被调用 verify(userService, times(1)).calculateTax(); } }2. Spring 集成测试使用SpyBean当你使用SpringBootTest时普通的Spy无法替换 Spring 容器里的 Bean。这时要用 Spring Boot 提供的SpyBean。作用把 Spring 容器里原本的 Bean 替换成一个 Spy 对象。SpringBootTest class OrderServiceIntegrationTest { Autowired private OrderService orderService; // 真实的 Service // 1. SpyBean替换容器里的 UserService但保留真实逻辑 SpyBean private UserService userService; Test void testOrderWithSpyBean() { // --- Arrange --- // 拦截 getUserLevel 方法返回 VIP doReturn(VIP).when(userService).getUserLevel(anyLong()); // --- Act --- // 调用 orderService它会调用 userService.getUserLevel // 此时 getUserLevel 返回 VIP但 userService 的其他方法如 saveUser仍走真实数据库逻辑如果配置了的话 orderService.createOrder(1L); // --- Verify --- // 验证 getUserLevel 确实被调用了 verify(userService).getUserLevel(1L); } }⚖️ Mock vs Spy怎么选为了让你更清晰地做决定我整理了这个对比表表格维度Mock (完全模拟)Spy / SpyBean (部分模拟)真实代码执行绝不执行默认执行(除非被拦截)初始化要求不需要实例化必须有真实实例 (new Object())存根语法when(mock.method())...doReturn(...).when(spy)...风险低完全隔离中真实代码可能抛异常或依赖数据库适用场景依赖对象Repository, Client被测对象本身想测部分逻辑、遗留代码⚖️ Spy vs InjectMocks维度SpyInjectMocks核心职责部分模拟。包装一个真实对象保留真实逻辑但允许拦截特定方法。依赖注入。创建被测对象并自动把Mock或Spy塞进去。代码行为默认执行真实代码。负责初始化对象通过构造函数或字段注入。语法陷阱必须手动初始化实例 new UserService()否则报错。不需要手动初始化Mockito 会自动帮你new出来。常用搭配用于被测对象本身当你不想 Mock 所有方法时。用于被测对象当你想完全隔离只测逻辑流转时。存根语法必须用doReturn(...).when(spy)...(它本身不存根它注入的对象如果是 Mock则用when...thenReturn) 避坑指南初始化陷阱使用Spy时字段必须手动初始化如 new UserService()否则 Mockito 无法创建 Spy 对象会报NullPointerException。Final 方法和 Mock 一样Spy 也无法 Spyfinal方法。调用final方法时永远执行真实代码无法拦截。自调用问题在 Spring 中如果一个 Bean 的方法 A 调用了同一个类的方法 Bthis.methodB()即使你 Spy 了方法 BA 调用 B 时走的也是真实逻辑Spy 的拦截可能失效因为 Spring AOP 代理机制。总结建议在单元测试中优先使用Mock因为它更干净、更安全。只有当你真的需要保留真实逻辑或者为了省事不想 Mock 所有依赖时才使用Spy。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2441663.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!