资讯动态

SpringBoot测试实战:从单元测试到集成测试的完整指南

发布时间:2026/10/9 8:13:55 来源:尧图企业网站定制
SpringBoot测试这个坑我前后踩了接近三年才算摸出点门道。网上的教程一搜一大把但大多数停留在“会写SpringBootTest就算会测试”的阶段真到项目里面跑起来各种版本兼容、上下文隔离、数据污染、环境依赖的问题能把人整得焦头烂额。尤其是Spring Boot 3.x出来以后版本差异带来的测试改造工作量基本上能把人劝退一整轮。这篇东西我打算把我自己在实际项目里跑测试的完整思路、落到代码里的细节、以及踩过的坑全部倒出来尽量做到给别人的脚手架里能直接抄作业的那种粒度。内容围绕SpringBoot测试展开从单元测试到数据访问层测试、再到完整集成测试一条线走完适合刚接触SpringBoot测试体系的同学照着搭也适合已经写了半年测试但总觉得哪哪不对的同行对照排查。1. 整体思路拆解先搞清楚SpringBoot测试到底要解决什么问题1.1 为什么SpringBoot测试不能只靠一个注解很多人刚开始写SpringBoot测试的时候脑子里只有一个词——SpringBootTest。不管测什么先把这个注解怼上去然后再往测试类里Autowired一堆东西。这种写法最直接的问题就是每跑一个测试方法都会把整个Spring容器完整启动一遍。Service、Mapper、消息队列、定时任务全部给你加载出来跑一个单元测试要等十几秒甚至几十秒项目规模上去以后这种写法根本跑不动而且只要任何一块依赖出问题所有测试全都跟着挂。SpringBoot真正想推的测试思路是“切片测试”。也就是说你测哪个层就只加载哪个层的上下文。测Controller就只加载Web层相关的Bean测数据访问就只加载JPA或MyBatis相关的自动配置测Service就干脆连Spring容器都不用启动直接用Mockito把依赖打桩。这样每个测试的执行速度会快一个数量级而且失败之后的定位也会清晰很多——到底是哪一层出问题跑对应切片就知道不至于一个底层方法改了整个项目四百多个测试全部红掉让人一脸懵。我在实际项目中体会最深的一点是测试的速度直接决定了团队的测试习惯养成。如果跑一次全套要十分钟那大家自然地就会少跑或不跑。如果把测试分成几层单层几秒钟跑完提交前顺手跑一下就成了自然动作。这个心理层面的差异比什么技术选型都重要。1.2 测试金字塔在SpringBoot项目里怎么落地软件工程里的测试金字塔理念放到SpringBoot项目中可以分成四层。最底层是纯单元测试只写Service工具方法或工具类的逻辑分支完全不需要Spring容器第二层是切片测试也就是WebMvcTest和DataJpaTest这一族只启动SpringMVC或数据访问相关的Bean第三层是集成测试用SpringBootTest把完整的应用上下文拉起来配合Testcontainers连接真实的中间件做端到端验证最顶层才是基于UI或接口的端到端测试这一层在SpringBoot项目里通常交给独立的自动化测试团队去维护我们日常写代码的时候不太会去碰。合理的比例大概是单元测试占六成、切片测试占两成、完整集成测试占两成。我见过不少项目的现状是反过来的八成测试都是SpringBootTest跑得慢不说出了错还要翻一大片日志才能定位。后面我给出的每个示例都会明确标注属于哪一层以及我为什么建议在这一层这么做。1.3 测试目录与基础依赖准备先把仓库建对一个最基础但很多人忽略的问题测试代码放哪里、依赖怎么引。SpringBoot默认的测试目录结构是src/test/java和src/test/resources这个目录结构不要改Maven和Gradle的默认约定都认它你改了以后会遇到资源文件加载不到、包名扫描错乱这些奇怪问题完全没有必要。依赖方面Maven项目需要引入spring-boot-starter-testdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency这一条会帮你带进来JUnit 5、Mockito、AssertJ、Spring Test、JSONAssert、JsonPath这些测试工具箱覆盖日常绝大部分需求。Gradle项目则是在build.gradle的dependencies里加一行testImplementation org.springframework.boot:spring-boot-starter-test版本不用写由Spring Boot的BOM统一管理。这里有个经常踩的坑Spring Boot 2.x时代这个starter里默认带的是JUnit 5、Mockito 4的版本组合Spring Boot 3.x则直接跳到JUnit 5.9、Mockito 5的版本组合如果你项目里手动指定了低版本Mockito就会出现运行时找不到类的异常后面我会专门说这个。2. 单元测试实操Service层与工具类应该脱离Spring容器来测2.1 单元测试为什么不应该启动Spring容器先说一个很反直觉的点Service层的单元测试我不建议启动Spring容器也不建议加SpringBootTest。启动容器意味着加载了大量无关的Bean而我们只是想验证一个Service类的业务逻辑是否正确这本质上应该是纯粹的Java方法测试。越贴近纯粹测试越快定位越准。就算你的Service里有很多Autowired的依赖也可以用Mockito把它们全部mock掉。举个例子我现在要测一个用户注册Service它依赖UserRepository、PasswordEncoder和MqProducer三个组件。单元测试的正确姿势是这样的ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserRepository userRepository; Mock private PasswordEncoder passwordEncoder; Mock private MqProducer mqProducer; InjectMocks private UserService userService; Test void shouldThrowWhenUsernameAlreadyExists() { String username admin; when(userRepository.findByUsername(username)).thenReturn(Optional.of(new User())); assertThatThrownBy(() - userService.register(username, 123456)) .isInstanceOf(BusinessException.class); } Test void shouldEncryptPasswordAndSendMessageOnRegister() { when(userRepository.findByUsername(newUser)).thenReturn(Optional.empty()); when(passwordEncoder.encode(123456)).thenReturn(encoded-password); userService.register(newUser, 123456); verify(mqProducer).sendMessage(anyString()); verify(userRepository).save(any(User.class)); } }看到没有这里用ExtendWith(MockitoExtension.class)激活Mockito用Mock声明被测类的依赖再用InjectMocks把Mock注入进去。整个过程完全不碰Spring容器毫秒级跑完。这种做法的价值在于当测试失败时你能够立刻判断是业务逻辑错了还是依赖出了问题不会被其他Bean牵连。2.2 Spring Boot 3.x版本下的Mockito版本差异与兼容问题提到“springboot版本太高”这个热词在测试这块真的非常贴切。Spring Boot 3.x里spring-boot-starter-test默认引入的是Mockito 5.x而Mockito 5和4最核心的区别是Mockito 5移除了对JDK代理Mock final类这个老机制的兼容默认采用Inline MockMaker来支持mock final类。这本来是好事但因为国内很多项目历史比较长还在Spring Boot 2.x时代就手动引入了mockito-core 3.x或者mockito-inline 2.x一升到Spring Boot 3.x就会遇到异常org.mockito.exceptions.base.MockitoException: Could not initialize inline Byte Buddy mock maker.或者运行测试时提示“Mockito cannot mock this class: class ...”原因是项目在依赖中手动排除掉了spring-boot-starter-test自带的Mockito版本。解决方法其实很简单不要手动指定Mockito版本让Spring Boot的BOM统一管理。非要在老项目里保留版本覆盖的话可以单独引入mockito-inline并保持版本号与mockito-core一致。还有一个常见的迁移问题是Spring Boot 3.x要求JDK 17以上如果开发环境还是JDK 8那就老老实实留在2.x别硬升。2.3 Mock静态方法、final方法时的操作要点JDK 21时代以后mock静态方法这块基本就是Mockito的Inline MockMaker在管。有静态方法需要mock的场景比如某个工具类public class SmsCodeGenerator { public static String generate() { return UUID.randomUUID().toString().replace(-, ); } }测试时我们想固定这个返回值Test void shouldUseFixedSmsCodeWhenRegister() { try (MockedStaticSmsCodeGenerator mocked mockStatic(SmsCodeGenerator.class)) { mocked.when(SmsCodeGenerator::generate).thenReturn(FIXED-CODE); UserService userService new UserService(); userService.setSmsCodeGenerator(new SmsCodeGenerator()); String result userService.registerWithSmsCode(张三); assertThat(result).contains(FIXED-CODE); } }要点在于MockedStatic必须在try-with-resources里使用保证每个测试方法结束之后静态mock被释放否则两个测试方法之间会相互污染。另外Mockito版本过低的时候mockStatic会直接报错所以如果你用到这功能务必确认实际生效的Mockito版本不低于3.4。3. Web层切片测试WebMvcTest与MockMvc怎么配合3.1 WebMvcTest到底切掉了什么Controller层的测试我推荐使用WebMvcTest它只加载SpringMVC的Web层上下文包括ControllerAdvice、HandlerInterceptor、Filter、Jackson配置这些东西而不会加载Service、Repository等底层组件。由于上下文变小启动速度快很多同时你可以用MockBeanWebMvcTest环境下推荐的mock注入方式把Service层的依赖mock掉从而只关注接口的入参出参、HTTP状态码等表现层逻辑。Spring Boot 3.4之后推荐用MockitoBean替代MockBean两者功能基本一致但新名字更贴切老代码继续用MockBean也没问题只是要注意Spring Framework 6.2起MockBean已经被标记为废弃等版本升级时最好顺手换成MockitoBean。3.2 MockMvc请求构造与JSON响应断言用MockMvc测Controller接口时请求体构造和响应断言是最容易出错的地方。请求体构造上我习惯用ObjectMapper把对象转成JSON字符串WebMvcTest(controllers UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; Autowired private ObjectMapper objectMapper; MockitoBean private UserService userService; Test void shouldReturnUserWhenGetUser() throws Exception { when(userService.getUserById(1L)).thenReturn(new User(1L, 张三, zhangsanexample.com)); mockMvc.perform(get(/api/users/1) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.name).value(张三)) .andExpect(jsonPath($.email).value(zhangsanexample.com)); } Test void shouldCreateUserWhenPostRequestIsValid() throws Exception { CreateUserRequest request new CreateUserRequest(李四, lisiexample.com); when(userService.createUser(any())).thenReturn(1L); mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(header().string(Location, /api/users/1)); } }jsonPath语法能力非常强数组长度、对象字段是否存在、嵌套字段类型都能断言。常用的几个$.name取字段值、$.items.length()取数组长度、$.items[0].id取数组第一个元素的字段。在断言时务必注意字段为空的情况Jackson默认会把值为null的字段也序列化出来如果你的业务里字段返回nulljsonPath断言这个字段存在仍然是能通过的。3.3 日期格式、BigDecimal序列化的坑Controller测试里还有一个高频坑日期时间格式。如果你在实体里用了LocalDateTimeJackson默认会序列化成数组格式[2024, 6, 1, 10, 30, 0]而不是2024-06-01 10:30:00。这时候你需要在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss或者手动在ObjectMapper里注册JavaTimeModule并设置格式化方式Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; }你不在全局ObjectMapper里做好格式化Controller测试里返回的JSON字符串就永远不会是你前端想要的样子这个问题测试环境不发现、联调时必炸。BigDecimal在JSON中会被序列化为数值如果你希望金额始终保留两位小数最好用JsonFormat(shape JsonFormat.Shape.STRING)强制转字符串保证精度。4. 数据访问层测试DataJpaTest Testcontainers才是实战组合4.1 DataJpaTest默认用H2的局限WebMvcTest解决了Web层数据访问层对应的切片测试是DataJpaTest。它默认只加载JPA相关的自动配置、EntityManager、DataSource和Spring Data JPA的Repository并会自动回滚事务测试方法之间数据互不干扰。听起来很完美但有个很大的坑它默认使用嵌入式数据库比如H2如果你的项目里SQL是PostgreSQL或MySQL风格H2在方言支持上会出现各种水土不服。举个我实际碰到的例子项目里有个查询用到了PostgreSQL的jsonb字段Query(value select * from user_info where extra_data - level :level, nativeQuery true) ListUserInfo findByLevel(Param(level) String level);H2不支持这种语法一跑就是语法错误。就算你硬要迁移到H2还得自己写兼容的SQL、改造测试数据这违背了测试的初衷。所以我现在的建议是凡是用了稍微不那么基础的数据库特性的项目数据访问测试直接用Testcontainers起真实数据库。4.2 Testcontainers配合MySQL容器的完整做法Testcontainers在SpringBoot项目里的用法已经很成熟了。引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-testcontainers/artifactId scopetest/scope /dependency dependency groupIdorg.testcontainers/groupId artifactIdjunit-jupiter/artifactId scopetest/scope /dependency dependency groupIdorg.testcontainers/groupId artifactIdmysql/artifactId scopetest/scope /dependency然后自定义一个测试配置类用Container注解声明MySQL容器Testcontainers DataJpaTest class UserRepositoryTest { Container ServiceConnection static MySQLContainer? mysql new MySQLContainer(mysql:8.0.36) .withDatabaseName(test_db) .withUsername(test) .withPassword(test); Autowired private UserRepository userRepository; Test void shouldFindUserByEmail() { User user new User(); user.setEmail(someoneexample.com); userRepository.save(user); OptionalUser found userRepository.findByEmail(someoneexample.com); assertThat(found).isPresent(); } }这里最关键的是Container和ServiceConnection两个注解的配合。Container负责启动容器ServiceConnection会把容器暴露的连接信息自动注入到测试上下文的数据源配置中。如果不写ServiceConnection也可以手动指定jdbcUrlstatic MySQLContainer? mysql new MySQLContainer(mysql:8.0.36); DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, mysql::getJdbcUrl); registry.add(spring.datasource.username, mysql::getUsername); registry.add(spring.datasource.password, mysql::getPassword); }两种方式我都在项目中用过ServiceConnection是Spring Boot 3.1之后提供的简化方案省去手动注册的麻烦但如果你用的Spring Boot版本低于3.1就得走DynamicPropertySource的老路。4.3 测试数据管理与事务回滚Transactional与DataJpaTest的默认行为DataJpaTest默认开启事务每个测试方法跑完自动回滚所以你在一个测试方法里save的数据不会污染下一个方法。但要注意如果你要测试的Service方法本身有事务传播比如它内部调用了另一个事务方法并提交那你再在测试方法里查已经提交的数据就会发现数据不见了。这种场景更合适的做法是集成测试而非切片测试。还有一点容易被忽略validate ddl策略。JPA默认会根据实体自动创建表结构所以DataJpaTest不需要建表脚本就能跑。但你如果偷懒在实体里漏标了字段类型比如本来该用LocalDateTime的字段写成了StringHibernate建表会按默认规则生成schema测试查询时就会出现类型转换异常这类问题在测试阶段就能暴露出来其实非常划算。5. 完整集成测试SpringBootTest TestRestTemplate验证最终效果5.1 需要完整启动容器的场景单元测试、切片测试覆盖完各模块的细节之后完整链路的验证还是要交给SpringBootTest。这个注解可以把完整的应用上下文拉起来并启动内嵌Servlet容器。我一般只在三种场景下用它验证跨模块的事务一致性、验证消息队列从接收到处理的完整流程、验证定时任务的调度逻辑。因为完整启动很慢这类测试的数量一定要控制住别养出一堆跑十分钟的测试。启动方式上推荐用随机端口SpringBootTest(webEnvironment SpringBootTest.WebEnvironment.RANDOM_PORT) class UserApiIntegrationTest { Autowired private TestRestTemplate restTemplate; Test void shouldReturnUserWhenCallApi() { ResponseEntityUser response restTemplate.getForEntity(/api/users/1, User.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getName()).isEqualTo(张三); } }用RANDOM_PORT而不是DEFINED_PORT可以避免测试环境端口冲突。测试类里SpringBootTest不加webEnvironment默认是MOCK模式只加载上下文但不启动真正的Servlet容器MockMvc在这种模式下也还能用但它本质上仍然在mock环境中和完整RANDOM_PORT模式有区别想看真实的HTTP响应包括Filter、拦截器、异常处理在工作中的链路还是得RANDOM_PORT。5.2 外部依赖消息队列、第三方接口怎么处理真实项目里完全脱离外部依赖的集成测试是不存在的。消息队列这边如果是ActiveMQTestcontainers也提供了对应的容器支持Testcontainers SpringBootTest class OrderIntegrationTest { Container ServiceConnection static ActiveMQContainer activeMQ new ActiveMQContainer(apache/activemq-classic:5.18.3); Autowired private OrderService orderService; Autowired private JmsTemplate jmsTemplate; }然后测试方法里既能发消息给Service消费也能直接断言到队列里是否收到了指定消息Test void shouldSendMessageWhenOrderCreated() throws Exception { orderService.createOrder(new CreateOrderRequest(ORD-001, 999.99)); Message message jmsTemplate.receive(order.created); assertThat(message).isNotNull(); assertThat(message.getBody(String.class)).contains(ORD-001); }第三方HTTP接口的mock我推荐用MockWebServer它是OkHttp团队出的测试工具不需要自己去写一个假的HTTP服务然后拼端口那些繁琐的配置MockWebServer server new MockWebServer(); BeforeEach void setUp() throws IOException { server.start(); server.enqueue(new MockResponse().setBody({\status\:\success\}).setResponseCode(200)); String baseUrl server.url(/).toString(); paymentClient.setBaseUrl(baseUrl); } Test void shouldMarkOrderPaidWhenThirdPartyReturnsSuccess() { PaymentResult result paymentClient.pay(ORD-001, 999.99); assertThat(result.isSuccess()).isTrue(); } AfterEach void tearDown() throws IOException { server.shutdown(); }MockWebServer的好处是实现简单不用额外依赖适合在单元测试和集成测试中模拟外部REST接口。另一个选项是WireMock它更接近真实服务的状态管理能支持多请求校验、stub热替换但配置复杂度也更高。日常项目里MockWebServer基本够用除非你对接口调用顺序有特别严格的校验需求那才值得上WireMock。5.3 测试数据工厂与清理策略集成测试跑起来之后数据梳理是最让人头疼的地方。我建议在测试工程里单独建一个“测试数据工厂”不要每写一个测试就在方法里手写new User()然后把每个字段都set一遍那写十几个测试就是灾难了。可以用一点简单的Builder或工厂类封装public final class UserFixtures { private UserFixtures() {} public static User defaultUser() { return User.builder() .username(zhangsan) .email(zhangsanexample.com) .password(encrypted-password) .status(Status.ACTIVE) .build(); } public static User userWithName(String username) { User user defaultUser(); user.setUsername(username); return user; } }数据清理方面SpringBootTest默认没有自动回滚事务。你可以在测试类上加Transactional实现回滚但这会导致一个问题被测代码里自己开启了新事务并提交了那么回滚逻辑就会失效。针对这种场景我常用的方案是在每个测试方法执行完成后用JdbcTemplate清掉相关表的数据AfterEach void cleanUp() { jdbcTemplate.update(DELETE FROM order_item); jdbcTemplate.update(DELETE FROM orders); jdbcTemplate.update(DELETE FROM users); }DELETE顺序需要按照外键依赖关系从子表删到父表否则会报外键冲突。这个清理策略虽然啰嗦但它可控。6. 常见问题与排查技巧实录6.1 SpringBoot版本升级带来的测试兼容性问题速查热搜词里反复出现的“springboot版本太高”在测试领域实际对应的是升级到3.x之后的一批兼容性问题。最典型的是下面这张表问题类型Spring Boot 2.xSpring Boot 3.x解决办法命名空间javax.persistencejakarta.persistence引包全局替换为jakartaMockito默认4.x默认5.x不要手动覆盖mockito版本尤其是mockito-core低版本MockBean地位长期使用标记废弃改用MockitoBean内嵌容器Tomcat 9Tomcat 10.1注意内嵌容器的Servlet API变化配置属性server.servlet.*server.servlet.*基本不变很多spring.*配置项改名需要对照官方迁移文档我升级时踩得最狠的一坑是换了Spring Boot 3之后测试里一直报ClassNotFound: javax/servlet/Filter排查了半天发现是IDE的缓存还在用旧的javax包后来把Maven仓库的旧jar清干净再重新导入就正常了。这类问题不是代码问题但是特别容易让人绕弯所以如果你升级完遇到奇奇怪怪找不到类的报错第一件事清理一下本地依赖缓存。6.2 测试上下文缓存与并行执行的冲突Spring Test框架默认会把相同的ApplicationContext缓存起来复用这是一个性能优化但也会带来副作用。比如两个测试类都用SpringBootTest启动了同一个配置但其中一个用MockWebServer改了外部服务的地址另一个测试类复用上下文时就会拿到错的连接信息。这时候可以在修改了上下文的那个测试类上加DirtiesContextDirtiesContext(classMode DirtiesContext.ClassMode.AFTER_CLASS) class PaymentIntegrationTest { // 这里改了外部服务地址或系统属性需要销毁上下文 }加了之后Spring会在当前测试类执行完后重新创建新的上下文从而避免影响后续测试。DirtiesContext也不是越多越好每加一次就多一次容器创建销毁的开销只有在确实修改了上下文状态的场景下才值得使用。并行执行是另一个容易踩坑的地方。Spring Boot 3.0之后JUnit 5支持Execution(PARALLEL)并行跑测试但数据库容器、Redis容器这些共享资源在并行时相互干扰非常明显。我的建议是刚开始不要追求全局并行可以并行不同模块的单元测试类但保持集成测试串行等基础设施都隔离好之后再逐步放开。6.3 测试偶发失败的排查思路测试偶发失败比稳定失败难处理得多因为它可能是资源竞争、随机时间、脏数据等共同作用的结果。我遇到最多的几个来源常见原因排查方式预防手段时间相关逻辑用了new Date()固定比较看日志中时间戳差几毫秒用Clock注入测试里固定时间UUID随机导致断言不稳定多次运行复现断言时不要精确匹配UUID改用匹配模式数据库连接池耗尽看连接池活跃数和等待时间调高测试库连接数或限制并行度测试数据未清理干净看失败用例是否存在跨类数据依赖AfterEach清理独立数据工厂偶发失败一旦连续出现两次以上就值得停下来排查不能当作flaky test放一边。顺手记一个检查顺序先看是不是只用一句断言就确认了结果、有没有在断言前做充分等待、有没有多个测试共用一份可变数据。这三个问题占了九成。6.4 逻辑快查一个Controller测试写不下去时的排查路径写Controller测试时很多人会碰到测试方法里Autowired MockMvc或Repository失败的问题。我按优先级给一个排查顺序先看测试类注解是不是WebMvcTest或SpringBootTest如果注解不对注入必然可能失败再看包扫描路径SpringBoot默认只扫启动类同包及子包的组件测试类所在包如果完全不同会扫描不到Bean最后看依赖注入方式切片测试里需要给Service打桩的依赖没有加MockitoBean也会导致上下文加载失败。7. 测试工程化落地建议7.1 覆盖率指标怎么定才合理覆盖率这个东西公司层面总喜欢定一个数字比如行覆盖率达到80%低于就发版告警。我自己的经验是核心业务模块的覆盖率值得盯但不要把每个getter、setter、配置类都算进去。如果目标覆盖率永远达不到团队就会被逼去写一堆这也不测那也不测、只为凑数字的测试毫无意义。我建议的目标是Service层和Controller层核心业务分支不低于70%数据访问层由于SQL复杂度和与数据库强依赖按实际场景写不必强撑一个数字工具类这种纯函数逻辑尽量到90%以上因为这种代码没有环境依赖是最值得测的。最终底线是线上出过的bug对应的场景必须保证补了回归测试。这比盯数字有用一百倍。7.2 测试命名与组织方式测试类命名我推荐被测试类名加Test后缀例如UserService的直接测试类叫UserServiceTestController层叫UserControllerTest。测试方法用中文方法名或下划线分隔的英文方法名都有实践我个人更偏向方法名直观表达业务场景命名风格例子适合场景英文风格shouldThrowExceptionWhenUserAlreadyExists团队里英文读写无障碍中文风格用户名重复时抛异常团队非英语母语为主沟通成本低下划线大小写混合createUser_shouldReturnId_whenInputValid老项目风格迁移成本低最终建议是团队统一一种风格而不是纠结哪种最好。JUnit 5里DisplayName注解可以辅助展示文件名这个如果不嫌麻烦也可以多用DisplayName(用户注册场景) class UserServiceTest { Test DisplayName(用户名重复时抛出BusinessException) void shouldThrowWhenUsernameExists() { } }7.3 把测试接入CI流水线测试写得再好如果只在本地跑价值也打对折。CI流水线里最起码要挂两个门槛合并请求触发单元测试和切片测试发布前触发完整集成测试。Testcontainers的场景在CI里有个痛点是Docker环境常见的GitLab Runner或Jenkins需要确保执行节点上有Docker可用。如果CI环境跑不了Docker你还有两个选择一个是把测试拆成两层单元测试和不需要容器的切片测试直接跑需要容器的集成测试放到独立Stage在专门的Docker节点上执行另一条路是引入testcontainers-cloud的远程容器服务但那个是商业方案还得看团队预算和网络条件。另一个CI细节是把测试报告和覆盖率报告归档。第一次配CI时我就忘了后来排查一起线上问题想看历史测试报告才发现CI里根本没保留。用Maven的话surefire插件默认在target/surefire-reports目录生成报告Gradle的build/reports/tests目录也有CI配置里把这两个目录作为artifact保存成本很低收益却很大。写在最后的几个实操心得从我个人的实践经验看SpringBoot测试最值得投入的三件事第一是花时间把测试依赖版本理清楚尤其是Spring Boot 3.x的新版本兼容问题在这个地方踩坑浪费的时间远大于节省的时间第二是掌握切片测试这种上下文隔离思路它能让你在测试速度和测试精度之间找到平衡点第三是接受“慢测试”的存在但要控制它出现的频率和范围而不是把所有测试都变成慢测试。最后再分享一个我最近在用的习惯每次写完一个功能点顺手把对应的测试写在同一个提交里而不是攒一批功能一起补测试。Commit里功能改动和测试改动在一起代码评审时能直观看到每个改动是否有配套保障后续回溯历史的时候也方便很多。这个习惯坚持半年你会发现测试不再是项目的负担而是你对改动有信心的来源。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑