ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot单元测试实战:JUnit5+Mockito+MockMvc黄金组合详解

SpringBoot单元测试实战:JUnit5+Mockito+MockMvc黄金组合详解 1. 项目概述与核心价值在开发一个SpringBoot应用时我们常常会陷入一种“功能跑通就行”的思维定式。直到某天你修改了一个看似无关紧要的工具类方法上线后却发现核心支付接口挂了排查半天才发现是某个Service的依赖注入出了问题。这种场景但凡做过几年后端开发的朋友应该都深有体会。单元测试就是对抗这种“蝴蝶效应”最有效的武器。它不是给QA团队写的而是我们开发者为自己代码的“健壮性”和“可维护性”买的一份保险。今天要聊的就是在SpringBoot环境下如何利用JUnit 5、MockMvc和Mockito这套“黄金组合”写出既高效又可靠的单元测试。JUnit 5是当前Java测试的事实标准提供了比JUnit 4更强大、更灵活的测试能力。Mockito则是模拟Mock领域的王者能让我们轻松隔离被测对象的外部依赖。而SpringBoot提供的MockMvc则是专门为测试Web层Controller而生的利器可以不用启动整个Web容器就能模拟HTTP请求并验证响应。这篇文章不会只给你一堆干巴巴的代码片段。我会结合我过去在多个项目中搭建测试框架的经验从为什么这么设计到具体每一步怎么操作再到实际踩过哪些坑给你讲透。目标是让你看完之后不仅能照着写出测试用例更能理解背后的设计思想从而在遇到更复杂的场景时也能游刃有余。2. 环境搭建与依赖配置2.1 核心依赖选型与解析一切始于pom.xml。依赖配置不对后面全是白费功夫。在SpringBoot 2.2及以上版本中官方已经默认集成了JUnit 5。但我们仍需要明确地引入一些必要的依赖以确保功能的完整性和版本的统一。首先是spring-boot-starter-test。这个starter是SpringBoot测试的“全家桶”它默认包含了JUnit 5、Mockito、AssertJ、Hamcrest等一整套测试库。对于绝大多数情况引入它就足够了。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency关键点解析这里的scopetest/scope至关重要。它意味着这些依赖只在运行测试mvn test或./gradlew test时生效不会被打进最终的生产包JAR/WAR保证了部署包的纯净和轻量。但是spring-boot-starter-test中包含的Mockito版本有时可能不是最新的或者你可能需要用到Mockito的一些高级特性如对静态方法、构造函数的Mock。这时可以显式地引入特定版本的Mockito依赖。注意要放在spring-boot-starter-test依赖的后面这样Maven/Gradle会优先使用我们指定的版本。dependency groupIdorg.mockito/groupId artifactIdmockito-inline/artifactId version5.2.0/version !-- 使用你需要的版本 -- scopetest/scope /dependencymockito-inline替代了旧的mockito-core它支持对final类、静态方法等的Mock功能更强大。如果你的项目中有很多工具类静态方法需要被Mock这个依赖会非常有用。2.2 测试类结构与注解解读依赖配好了我们来创建第一个测试类。假设我们有一个UserService需要测试。在src/test/java的对应包路径下创建UserServiceTest.java。一个标准的、基于SpringBoot的测试类通常会用到以下几个核心注解import org.junit.jupiter.api.Test; // JUnit5的注解 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; SpringBootTest // 标记1SpringBoot测试的基石 class UserServiceTest { Autowired // 标记2注入真实的被测对象 private UserService userService; MockBean // 标记3注入一个Mock的依赖对象 private UserRepository userRepository; Test // 标记4声明这是一个测试方法 void testGetUserById_Success() { // 测试逻辑在这里编写 } }我们来逐一拆解这些注解SpringBootTest这是整个测试的“启动器”。它会加载完整的Spring应用上下文ApplicationContext就像启动了一个迷你版的真实应用。你可以通过webEnvironment属性来指定Web环境例如SpringBootTest(webEnvironment SpringBootTest.WebEnvironment.NONE)表示不启动Web容器适合纯Service层测试RANDOM_PORT或DEFINED_PORT则会启动一个嵌入式的Web服务器如Tomcat。Autowired这里注入的是真实的UserService实例。我们的目标是测试这个真实对象的行为。Spring会帮我们完成所有的依赖注入和Bean的生命周期管理。MockBean这是SpringBoot对Mockito的集成注解。它会在Spring的应用上下文中用一个Mockito创建的Mock对象替换掉原有UserRepository类型的Bean。这样UserService中依赖的UserRepository就被我们完全控制了我们可以任意定义这个Mock对象的行为即“打桩”从而将测试焦点完全隔离在UserService自身的逻辑上。Test(JUnit Jupiter)来自JUnit 5标识一个测试方法。JUnit 5的包路径是org.junit.jupiter.api注意和JUnit 4的org.junit.Test区分开。实操心得对于纯粹的、不涉及Spring容器功能的工具类或算法类测试其实不需要使用SpringBootTest。直接使用JUnit 5的Test注解创建一个普通的Java测试类即可这样测试运行速度会快得多。SpringBootTest更适合集成测试或需要测试Spring特定功能如事务、AOP的场景。3. Mockito核心技巧模拟与验证的艺术单元测试的核心思想是“隔离”。Mockito就是帮助我们实现隔离的瑞士军刀。它的工作主要分为两部分行为模拟Stubbing和交互验证Verification。3.1 行为模拟告诉Mock对象“当…时请做…”假设我们的UserService有一个方法getUserById(Long id)内部会调用userRepository.findById(id)。在测试中我们不想真的去查数据库就需要模拟这个调用并返回我们预设的结果。Test void testGetUserById_Success() { // 1. 准备测试数据 Long userId 1L; User expectedUser new User(userId, 张三, zhangsanexample.com); // 2. 定义Mock对象的行为打桩 // 含义当 userRepository.findById(1L) 被调用时返回一个包含 expectedUser 的 Optional when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser)); // 3. 执行被测方法 User actualUser userService.getUserById(userId); // 4. 断言结果 assertNotNull(actualUser); assertEquals(张三, actualUser.getName()); assertEquals(userId, actualUser.getId()); }when(...).thenReturn(...)是Mockito最经典的语法。它清晰地定义了“当发生某个调用时返回某个值”。这里模拟的是findById返回Optional.of(user)这是Repository层常见的返回类型对应数据库中查找到记录的情况。更复杂的模拟场景模拟异常测试当依赖抛出异常时你的服务是否处理得当。when(userRepository.findById(anyLong())).thenThrow(new RuntimeException(数据库连接失败)); assertThrows(ServiceException.class, () - userService.getUserById(1L));thenThrow用于模拟方法抛出异常。anyLong()是一个参数匹配器表示任何Long类型的参数都会触发这个行为。模拟连续调用同一个方法第一次调用和第二次调用返回不同的值。when(mockList.get(0)) .thenReturn(first) // 第一次返回first .thenReturn(second); // 第二次及以后返回second模拟void方法对于无返回值的方法使用doNothing()、doThrow()。doNothing().when(userRepository).deleteById(userId); // 或者模拟删除时抛出异常 doThrow(new IllegalArgumentException(无效ID)).when(userRepository).deleteById(-1L);3.2 交互验证确认Mock对象“被如何调用”行为模拟保证了我们能有确定的输入输出。交互验证则用于确保被测对象与依赖对象之间的协作符合预期。这是单元测试中非常强大的一环能帮你发现一些逻辑错误。继续上面的例子假设getUserById方法在找到用户后还会调用一个auditService.logAccess(userId)方法来记录访问日志。我们想验证这个调用确实发生了。MockBean private AuditService auditService; Test void testGetUserById_VerifyInteraction() { Long userId 1L; User mockUser new User(userId, 李四, lisiexample.com); when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); userService.getUserById(userId); // 验证auditService的logAccess方法被调用了一次且参数是userId verify(auditService, times(1)).logAccess(userId); // 验证userRepository的findById方法被调用了一次且参数是userId verify(userRepository, times(1)).findById(userId); // 验证在调用userService.getUserById之后auditService没有其他任何交互 verifyNoMoreInteractions(auditService); }verify(mockObject, verificationMode).methodName(arguments)这是验证的核心语法。times(1)是验证模式表示期望被调用恰好一次。类似的还有never()从未调用、atLeastOnce()至少一次、atLeast(3)至少三次等。verifyNoMoreInteractions(auditService)是一个严格的验证它确保在验证点之后这个Mock对象没有再发生任何未经验证的交互。这有助于发现多余的、意外的调用。注意事项交互验证要谨慎使用不要过度验证。单元测试应该聚焦于被测对象的状态输出结果而非其实现细节内部调用顺序。过度验证会导致测试变得脆弱一旦内部实现重构比如调整了方法调用顺序即使功能正确测试也会失败。通常验证那些具有业务意义的副作用如发送消息、记录日志是合适的。3.3 参数匹配器与注解简化Mockito提供了丰富的参数匹配器Argument Matchers让我们的模拟和验证更加灵活。any()anyString()anyLong()匹配任何对应类型的参数。eq(value)匹配一个具体的值。当使用参数匹配器时所有参数都必须使用匹配器如果想对某个参数使用具体值需要用eq()包裹。// 错误混合使用匹配器和具体值 // when(repo.save(any(), “固定值”)).thenReturn(...); // 正确使用eq()包裹具体值 when(repo.save(any(User.class), eq(“固定值”))).thenReturn(...);Mock与InjectMocks在非Spring环境中即不使用SpringBootTest和MockBean时你可以使用这对注解。Mock创建一个Mock对象InjectMocks会创建被测类的实例并自动将Mock标注的依赖注入进去。这在测试纯POJO服务时非常高效。import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.junit.jupiter.api.extension.ExtendWith; ExtendWith(MockitoExtension.class) // 启用Mockito支持 class PureServiceTest { Mock private Dependency dependency; InjectMocks private PureService pureService; // 会自动将dependency注入 Test void test() { when(dependency.call()).thenReturn(“mocked”); assertEquals(“mocked_result”, pureService.doSomething()); } }4. MockMvc深度应用Web层测试实战对于Spring MVC的Controller层测试如果每次都启动整个Web服务器速度慢且难以隔离。MockMvc正是为解决这个问题而生。它可以模拟Servlet容器直接向Controller发起HTTP请求并校验响应速度极快。4.1 初始化MockMvc的三种姿势根据测试的粒度有三种常见的初始化方式WebMvcTest(切片测试 - 推荐)这是测试Controller层的首选。它只会加载Web层相关的Bean如Controller,RestController,ControllerAdvice,JsonComponent,WebMvcConfigurer等而不会加载完整的应用上下文如Service,Repository。这提供了极佳的隔离性和启动速度。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; WebMvcTest(UserController.class) // 只加载UserController这一个Controller class UserControllerTest { Autowired private MockMvc mockMvc; // 自动注入MockMvc实例 MockBean // 因为WebMvcTest不加载Service所以Controller依赖的Service需要用MockBean模拟 private UserService userService; Test void shouldReturnUser() throws Exception { // ... 测试逻辑 } }SpringBootTestAutoConfigureMockMvc(集成测试)当你需要测试的Controller依赖了完整的Spring上下文比如使用了复杂的AOP拦截器、自定义过滤器链等或者你想同时测试多个Controller的集成效果时可以使用这种方式。SpringBootTest // 加载完整上下文 AutoConfigureMockMvc // 自动配置MockMvc class UserControllerIntegrationTest { Autowired private MockMvc mockMvc; // MockBean 依然可以在这里使用来模拟某些特定的Bean }手动构建在极少数需要完全控制MockMvc配置的情况下使用。MyController controller new MyController(); MockMvc mockMvc MockMvcBuilders.standaloneSetup(controller).build();4.2 发起请求与断言响应初始化好mockMvc后就可以编写测试了。其核心是链式调用非常符合“构建-执行-断言”的模式。Test void getUserById_ShouldReturnUser_WhenUserExists() throws Exception { // 1. 模拟Service层行为 Long userId 100L; User mockUser new User(userId, 王五, “wangwuexample.com”); when(userService.getUserById(userId)).thenReturn(mockUser); // 2. 构建并执行HTTP GET请求 mockMvc.perform(MockMvcRequestBuilders.get(“/api/users/{id}”, userId) // 请求方法URL路径变量 .contentType(MediaType.APPLICATION_JSON) // 设置请求头 .accept(MediaType.APPLICATION_JSON)) // 设置接受类型 // 3. 对响应进行断言 .andExpect(MockMvcResultMatchers.status().isOk()) // 断言HTTP状态码为200 .andExpect(MockMvcResultMatchers.jsonPath(“$.id”).value(userId)) // 使用JsonPath断言JSON响应体 .andExpect(MockMvcResultMatchers.jsonPath(“$.name”).value(“王五”)) .andExpect(MockMvcResultMatchers.jsonPath(“$.email”).value(“wangwuexample.com”)); }perform()发起一个请求。其参数通过MockMvcRequestBuilders的静态方法get,post,put,delete等构建。andExpect()添加一个断言。可以链式调用多个。断言工具在MockMvcResultMatchers中。jsonPath(“$.id”)这是一个非常强大的功能它使用JsonPath表达式类似于XPath for JSON来定位和验证JSON响应体中的特定字段。$表示根节点。测试POST请求带请求体Test void createUser_ShouldReturnCreatedUser() throws Exception { User newUser new User(null, “赵六”, “zhaoliuexample.com”); User savedUser new User(200L, “赵六”, “zhaoliuexample.com”); when(userService.createUser(any(User.class))).thenReturn(savedUser); // 使用ObjectMapper将对象序列化为JSON字符串 ObjectMapper objectMapper new ObjectMapper(); String userJson objectMapper.writeValueAsString(newUser); mockMvc.perform(MockMvcRequestBuilders.post(“/api/users”) .contentType(MediaType.APPLICATION_JSON) .content(userJson)) // 设置请求体 .andExpect(status().isCreated()) // 断言201 Created .andExpect(jsonPath(“$.id”).exists()) // 断言返回的JSON中有id字段 .andExpect(jsonPath(“$.name”).value(“赵六”)); }4.3 处理请求参数、会话与安全MockMvc也能很好地模拟复杂的Web场景。查询参数与表单参数// GET /api/users?name张三age25 mockMvc.perform(get(“/api/users”) .param(“name”, “张三”) // 添加查询参数 .param(“age”, “25”)) // POST 表单提交 mockMvc.perform(post(“/login”) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param(“username”, “admin”) .param(“password”, “123456”))会话Session与CookiemockMvc.perform(get(“/cart”) .sessionAttr(“userId”, 123L)) // 设置Session属性 mockMvc.perform(get(“/profile”) .cookie(new Cookie(“token”, “abc123”))) // 设置Cookie测试安全控制器Spring Security 当你的Controller方法受Spring Security保护时需要在测试中模拟一个已认证的用户。Test WithMockUser(username “testUser”, roles {“USER”}) // 关键注解模拟一个具有指定角色和用户名的认证用户 void getMyProfile_ShouldReturnProfile_WhenAuthenticated() throws Exception { mockMvc.perform(get(“/api/my-profile”)) .andExpect(status().isOk()); } // 或者更灵活地使用SecurityMockMvcRequestPostProcessors Test void getMyProfile_WithRequestPostProcessor() throws Exception { mockMvc.perform(get(“/api/my-profile”) .with(SecurityMockMvcRequestPostProcessors.user(“admin”).roles(“ADMIN”))) .andExpect(status().isOk()); }5. JUnit 5进阶特性与测试组织JUnit 5不仅仅是一个测试运行器它提供了一套丰富的编程模型来组织我们的测试。5.1 生命周期注解控制测试的各个阶段JUnit 5提供了更细粒度的生命周期控制替代了JUnit 4的Before和After。import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.AfterAll; class UserServiceTest { BeforeAll // 在整个测试类开始前执行一次必须是static方法 static void initAll() { System.out.println(“初始化全局资源如数据库连接池”); } BeforeEach // 在每个Test方法执行前执行 void init() { System.out.println(“初始化测试数据重置Mock状态”); // 重要重置Mock对象的状态避免测试间相互影响 Mockito.reset(userRepository, auditService); } AfterEach // 在每个Test方法执行后执行 void tearDown() { System.out.println(“清理测试数据”); } AfterAll // 在整个测试类结束后执行一次必须是static方法 static void tearDownAll() { System.out.println(“释放全局资源”); } }实操心得BeforeEach中重置Mock对象Mockito.reset(...)是一个好习惯。因为Mock对象的行为when(...).thenReturn(...)和调用记录verify(...)在测试间是可能保持状态的重置可以确保每个测试方法都在一个干净、独立的环境下运行。5.2 断言库的升级AssertJ vs Hamcrestspring-boot-starter-test默认引入了AssertJ和Hamcrest。我个人强烈推荐使用AssertJ因为它提供了流式APIFluent API断言写起来更自然、可读性更强并且错误信息更友好。import static org.assertj.core.api.Assertions.*; Test void testWithAssertJ() { User user userService.getUserById(1L); // 流式断言非常直观 assertThat(user) .isNotNull() .hasFieldOrPropertyWithValue(“name”, “张三”) // 断言字段值 .hasNoNullFieldsOrProperties(); // 断言所有字段非空 ListUser userList userService.getAllUsers(); assertThat(userList) .isNotEmpty() .hasSize(2) .extracting(User::getName) // 提取对象列表中的某个属性形成新的断言列表 .containsExactly(“张三”, “李四”); // 精确匹配顺序和内容 // 异常断言 assertThatThrownBy(() - userService.deleteUser(-1L)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining(“无效ID”); }相比之下JUnit 5自带的Assertions和Hamcrest的语法就显得有些陈旧和繁琐了。AssertJ几乎能满足你所有的断言需求。5.3 参数化测试用一组数据驱动同一个测试当你想用多组不同的输入参数来运行同一个测试逻辑时参数化测试Parameterized Test能极大减少重复代码。import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; class ParameterizedTestExample { ParameterizedTest ValueSource(strings {“”, “ “, “\t”, “\n”}) // 提供参数源一组字符串 void isBlank_ShouldReturnTrueForAllBlankStrings(String input) { assertTrue(StringUtils.isBlank(input)); } ParameterizedTest CsvSource({ // 提供CSV格式的参数源适合多参数 “1, 张三, true”, “2, 李四, false”, “3, ‘’, true” // 空字符串用单引号包裹 }) void testUserWithCsv(Long id, String name, boolean shouldBeEmpty) { User user new User(id, name); assertEquals(shouldBeEmpty, user.getName().trim().isEmpty()); } // 使用一个返回Stream的方法作为参数源 static StreamArguments provideUsersForTest() { return Stream.of( Arguments.of(1L, “Alice”, 25), Arguments.of(2L, “Bob”, 30) ); } ParameterizedTest MethodSource(“provideUsersForTest”) // 引用上面的方法 void testWithMethodSource(Long id, String name, Integer age) { // ... 测试逻辑 } }参数化测试让“边界条件测试”和“不同输入组合测试”变得非常容易组织和维护。5.4 测试标签与过滤管理大型测试套件当项目变大测试类越来越多时你可能希望分组运行测试。JUnit 5提供了Tag注解。import org.junit.jupiter.api.Tag; Test Tag(“fast”) // 标记为快速测试 void fastTest() { // 执行很快的单元测试 } Test Tag(“slow”) // 标记为慢速测试 Tag(“integration”) // 可以打多个标签 void integrationTest() { // 执行较慢的集成测试 }然后你可以在Maven Surefire插件配置或IDE的运行配置中选择只运行特定标签的测试。!-- 在pom.xml中配置Maven只运行‘fast’标签的测试 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration groupsfast/groups /configuration /plugin这样在持续集成CI流水线中可以配置每次提交都运行fast测试而slow或integration测试只在夜间构建或发布前运行。6. 常见问题排查与实战技巧理论讲得再多不如实战中踩几个坑来得深刻。下面是我在项目中积累的一些典型问题和处理技巧。6.1 依赖注入失败NoSuchBeanDefinitionException这是最常见的问题之一。测试启动时Spring抱怨找不到某个Bean。场景在WebMvcTest(UserController.class)中UserController依赖了一个Component注解的TokenValidator但这个类没有被自动扫描到。原因WebMvcTest默认只扫描Controller,RestController等Web相关的注解。Component,Service,Repository等不会被扫描。解决方案使用MockBean如果这个依赖只是需要被Mock那么直接在测试类中用MockBean声明它。WebMvcTest(UserController.class) class UserControllerTest { MockBean private TokenValidator tokenValidator; // Spring会用它替换掉真实的Bean // ... }使用Import如果这个组件是测试必须的真实组件比如一个配置类里的Bean可以使用Import注解显式导入。WebMvcTest(UserController.class) Import({SecurityConfig.class, TokenValidator.class}) // 导入配置类和组件 class UserControllerTest { Autowired // 现在可以注入真实的TokenValidator了 private TokenValidator tokenValidator; }改用SpringBootTest如果依赖关系非常复杂切片测试配置起来太麻烦不如直接使用SpringBootTest加载完整上下文虽然启动慢点但一劳永逸。6.2 事务回滚测试数据污染数据库如果你的测试涉及真实的数据库操作比如使用DataJpaTest或SpringBootTest默认情况下Spring会用Transactional包裹每个测试方法测试结束后自动回滚保证数据库干净。问题但有时你会发现数据没有回滚或者你希望测试数据被提交例如想测试一个跨方法的事务边界。解决方案确认配置确保测试类或方法上没有使用Rollback(false)或Transactional(propagation Propagation.NOT_SUPPORTED)等注解。手动控制如果确实需要提交数据可以在方法上使用Commit注解。Test Commit // 这个方法的事务会被提交 void testWithCommit() { // ... 操作数据库 }使用DirtiesContext如果一个测试方法修改了Spring容器的状态比如修改了单例Bean的属性可能会影响后续测试。可以在该方法或类上标注DirtiesContext告诉Spring在测试结束后重新加载上下文。Test DirtiesContext // 测试结束后Spring上下文会被标记为“脏”下一个测试会使用新的上下文 void testThatModifiesSharedBean() { // ... }6.3 静态方法MockPowerMock的替代方案老项目中常有静态工具类方法。过去我们依赖PowerMock来Mock静态方法但它与JUnit 5和最新Mockito的兼容性常有麻烦。现代方案重构代码首选考虑将静态方法调用包装到一个非静态的Service或组件中然后Mock这个组件。这是最符合依赖注入原则的做法。使用Mockito的mockStatic(需要mockito-inline)Mockito 3.4.0 通过mockito-inline依赖支持了对静态方法的临时Mock。try (MockedStaticMyStaticUtils mockedStatic Mockito.mockStatic(MyStaticUtils.class)) { // 在这个try-with-resources块内静态方法被Mock mockedStatic.when(() - MyStaticUtils.generateId()).thenReturn(“fixed-id”); // 调用被测代码其内部调用的MyStaticUtils.generateId()将返回”fixed-id” String result myService.doSomething(); assertEquals(“result-with-fixed-id”, result); } // 块结束后静态方法的Mock自动失效恢复原样这种方式是线程安全的且Mock范围精确可控推荐使用。6.4 测试覆盖率与持续集成写测试不是为了完成任务而是为了保障质量。如何衡量测试的好坏测试覆盖率是一个重要但不是唯一的指标。使用Jacoco生成覆盖率报告 在pom.xml中添加Jacoco插件plugin groupIdorg.jacoco/groupId artifactIdjacoco-maven-plugin/artifactId version0.8.10/version !-- 使用最新版本 -- executions execution goals goalprepare-agent/goal /goals /execution execution idreport/id phaseverify/phase goals goalreport/goal /goals /execution /executions /plugin运行mvn clean verify后会在target/site/jacoco/目录下生成HTML格式的覆盖率报告。你可以看到行覆盖率、分支覆盖率等详细数据。集成到CI/CD在Jenkins、GitLab CI等工具中可以将覆盖率报告作为质量门禁。例如设置合并请求Merge Request要求行覆盖率不低于80%否则无法合并。这能促使团队养成良好的测试习惯。最后的心得单元测试不是负担而是投资。初期编写测试会花费一些时间但它带来的回报是巨大的更快的缺陷反馈、更安全的代码重构、更清晰的设计难以测试的代码通常意味着高耦合、以及作为代码行为的最新文档。从今天开始为你写的每一段核心业务逻辑配上几个测试用例吧。当你某次重构后一键运行所有测试并全部通过时那种信心和踏实感是任何文档都无法给予的。
返回列表