Spring Boot Test实战指南:从单元测试到集成测试的完整体系 写Spring Boot项目这些年我一直觉得测试是大多数人最容易忽略、也最容易“糊弄”过去的环节。很多工程团队对测试的态度是“能跑就行”直到线上出了事故才发现根本没有测试兜底。我刚接手的一个老项目就是典型改了个事务边界结果测试环境数据被清掉大半张表从那之后我才认认真真把SpringBoot Test这套测试体系从头摸了一遍。这篇文章就结合我这几年的实战经验把SpringBoot Test的核心机制、分层测试写法、集成测试方案、常见坑点完整梳理一遍希望能帮你在自己的项目里把这套体系真正用起来而不是只停留在“会写Test”的阶段。1. SpringBoot Test的整体设计思路与核心机制1.1 为什么需要一套独立的测试体系先理清一个基础问题JUnit本身已经能写测试了为什么还需要SpringBoot TestJUnit的定位是“最小化的测试框架”它负责的是怎么组织用例、怎么断言、怎么统计执行结果。但它完全不认识Spring容器也不知道你的Service、Mapper是怎么装配出来的。在实际项目中一个Service往往依赖了十几个Bean有数据源、有Redis客户端、有MQ生产者、有各种第三方客户端。如果你想在JUnit里手动new一个Service出来光是初始化依赖就要写一大堆代码而且这些手动创建的依赖跟生产环境里Spring管理的Bean状态并不一致测试结果自然不可信。SpringBoot Test解决的就是这个核心矛盾它继承JUnit的用例组织能力同时接管Spring容器的启动、Bean装配、配置加载、事务管理让测试用例能直接拿到由Spring容器创建的真实Bean。测试对象本身就是生产环境中那一套依赖关系而不是手工拼凑的影子对象。1.2 SpringBootTest注解的加载全过程SpringBootTest是整个SpringBoot Test体系的核心入口。它做的事情简单说就是“在测试运行前帮我们启动一个Spring容器”。启动过程大致是这样的通过SpringBootConfiguration自动搜索配置类。如果你的测试类在项目的根包下Spring Boot会向上扫描找到标注了SpringBootApplication的启动类以它作为配置源。根据配置类自动装配所有相关的Bean。注意这里不是“创建所有组件”而是执行完整的自动装配流程跟正常启动应用几乎一样。将容器中对应的Bean注入到测试类中通过Autowired或者构造器注入。SpringBootTest有一个关键属性是webEnvironment它决定了启动的Web环境类型。我见过不少人在这一步踩坑对这几个取值一定要清楚取值行为说明适用场景MOCK默认加载WebApplicationContext但不启动真实的嵌入式服务器使用MockServletContext大部分单元测试、Controller测试RANDOM_PORT启动真实的嵌入式服务器端口随机避免冲突集成测试、需要真实HTTP请求的场景DEFINED_PORT启动真实的嵌入式服务器使用配置文件中指定的端口需要固定端口调试的场景NONE不加载Web环境上下文Service层测试、不涉及Web的测试我自己的建议是默认情况下用MOCK就够只有在验证过滤器链、拦截器、真实HTTP请求时才考虑RANDOM_PORT。因为启动真实服务器会显著增加测试耗时。1.3 测试配置的加载优先级SpringBoot Test在加载配置时有一套优先级规则理解它才能控制好测试环境。测试过程中配置来源优先级从高到低大致为TestPropertySource注解指定的配置测试类所在模块的src/test/resources下的application.yml或application.propertiessrc/main/resources下的主配置也就是说测试目录下的配置会覆盖主配置但TestPropertySource又能覆盖测试配置。这个机制非常实用。比如你的主配置里连的是生产环境的数据库地址——虽然正常情况不该这么写但总有意外——测试环境下用src/test/resources/application.yml指向本地数据库或嵌入式数据库就能避免测试把数据写到真实环境。另外还有一个DynamicPropertySource注解它可以在测试运行过程中动态添加配置属性。这个在配合Testcontainers时非常有用因为容器的端口是运行后才确定的只能动态注册。DynamicPropertySource static void registerPgProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, postgresContainer::getJdbcUrl); registry.add(spring.datasource.username, postgresContainer::getUsername); registry.add(spring.datasource.password, postgresContainer::getPassword); }1.4 测试切片从全量加载到精准加载SpringBootTest设计上有个明显劣势每次跑测试都会加载整个应用的全部Bean。项目规模大了之后一次完整加载可能要花十几秒甚至几十秒跑一个模块的用例大部分时间都浪费在容器启动上。Spring Boot为此提供了“测试切片”机制也就是只加载测试目标所需的那一小部分Bean大幅缩短加载时间。目前常用的切片注解包括WebMvcTest只加载Web层包括Controller、Filter、拦截器、ControllerAdvice等不加载Service和Mapper层的Bean。DataJpaTest只加载JPA相关的组件包括Repository、EntityManager、DataSource等。JsonTest只加载Jackson或Gson的序列化组件用于测试JSON序列化。RestClientTest用于测试RestTemplate或WebClient的Rest客户端。切片测试的本质是利用Spring Boot的自动配置裁剪能力只启动目标场景相关的自动配置类。代价是切片测试里默认没有完整的Bean依赖链比如WebMvcTest里没有Service层的Bean必须通过MockBean或MockitoBean来补齐。我在项目里的习惯是Controller层、JSON序列化这类“薄层”用切片测试跑得快Service层直接用SpringBootTest加载真实上下文保证依赖完整集成测试才用RANDOM_PORT 真实中间件。2. 分层测试怎么写才不算白写2.1 Controller层测试WebMvcTest MockMvcController层是Web应用的入口它承担的是参数解析、请求分发、响应组装这些工作最核心的验证点有两个路由是否匹配、参数绑定和校验是否正确。用WebMvcTest加MockMvc来测试不需要启动真实服务器直接在Spring MVC的Mock环境中发起请求。WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockitoBean private UserService userService; Test void shouldReturnUserById() throws Exception { Long userId 1L; UserVO mockUser new UserVO(userId, 张三, zhangsanexample.com); when(userService.getUserById(userId)).thenReturn(mockUser); mockMvc.perform(get(/api/users/{id}, userId) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.data.id).value(1L)) .andExpect(jsonPath($.data.name).value(张三)); } }这里有个关键点WebMvcTest默认只会扫描被测试的Controller不会把所有Controller都加载进来。如果你测试的Controller里注入了其他组件比如全局异常处理器、拦截器可以通过Import显式引入或者用MockitoBean把无关依赖Mock掉。MockMvc提供了链式断言可以在一个用例里同时验证多个维度非常方便。除了基础的状态码和响应体还可以用jsonPath做精细化的字段断言。当你需要校验接口返回列表里的某个元素、某个嵌套字段或者校验响应头这些断言方式会非常有用。2.2 Service层测试SpringBootTest 事务回滚Service层承载核心业务逻辑它依赖Manager、Mapper、外部服务等。这一层我一般直接用SpringBootTest加载完整Spring上下文不是为了追求速度而是为了在更接近真实环境的前提下验证业务逻辑。Service层测试一个最有用的技巧是使用Transactional。给测试方法加上这个注解后Spring会在测试结束时自动回滚事务不会污染数据库。这意味着你可以放心地在测试方法里插入数据、修改数据、删除数据跑完之后数据库还是原样。SpringBootTest Transactional class OrderServiceTest { Autowired private OrderService orderService; Autowired private OrderRepository orderRepository; Test void shouldCreateOrderFromCart() { Cart cart new Cart(); cart.addItem(new CartItem(P001, 2)); cart.addItem(new CartItem(P002, 1)); Order order orderService.createOrder(1L, cart); assertNotNull(order.getId()); assertEquals(2, order.getItems().size()); assertEquals(3, order.getTotalQuantity()); } }但这里有一个非常隐蔽的坑Transactional在测试方法上生效的前提是Spring的代理机制。如果测试方法内部通过this调用同类中的另一个事务方法或者是private方法事务注解不会生效。更常见的一个坑是你在测试方法里开了事务又去验证一个事务内提交后才能看到的效果比如异步操作的MQ消息发送、定时任务触发就会发现问题。测试的事务会被独立开启而异步线程不会自动加入这个事务导致数据状态不一致。所以在Service层测试中我的建议是测试的预期边界尽量控制在同步、单事务的范围内。涉及异步、MQ、分布式事务的场景优先使用集成测试而不是依赖Transactional回滚。对包含复杂事务传播行为的代码不要轻信回滚要单独用例验证提交后的最终状态。2.3 数据访问层DataJpaTest与MyBatis的差异数据访问层测试直接面对数据库核心验证点是SQL是否正确、映射是否完整、持久化行为是否符合预期。如果项目用的是Spring Data JPA可以直接用DataJpaTest。它只加载JPA相关组件默认使用嵌入式内存数据库H2等并且每个测试方法都会自动回滚事务。DataJpaTest class UserRepositoryTest { Autowired private UserRepository userRepository; Test void shouldFindUserByEmail() { User user new User(); user.setEmail(testexample.com); userRepository.save(user); OptionalUser found userRepository.findByEmail(testexample.com); assertTrue(found.isPresent()); assertEquals(testexample.com, found.get().getEmail()); } }注意一个细节默认的DataJpaTest不会执行Flyway或Liquibase的数据库迁移脚本。如果你的表结构依赖迁移脚本需要显式加上AutoConfigureTestDatabase(replace Replace.NONE)来使用真实的数据源或者通过TestPropertySource指定Flyway的配置。如果项目用的是MyBatis就没有官方对应的切片注解。MyBatis的Mapper测试通常还是用SpringBootTest配合Transactional来做或者自己写一个轻量级的自动配置只加载SqlSessionFactory和对应的Mapper。不过MyBatis的Mapper.xml文件与Java接口的对应关系、动态SQL的编写是否正确是测试中最值得投入时间的地方。尤其是动态SQL最怕出现语法错误和参数绑定错误这类问题只有真正执行SQL才能发现。2.4 缓存、异步与定时任务的测试处理这三个场景是Service层测试里最容易“测了个寂寞”的环节。先谈缓存。Spring Cache的注解在测试中并不会自动失效如果某个方法上有Cacheable你第一次调用后第二次调用就直接走缓存了。想在测试中验证缓存行为有两种思路一是直接用CacheEvict或CacheManager手动清除缓存二是用EnableCaching(proxyTargetClass true)配合缓存切面来验证。我这里习惯的做法是在测试类上引入一个测试专用的配置类注册一个简单的Caffeine缓存管理器并预先清空缓存。异步方法的测试更麻烦。当你调用一个Async方法时主线程不会等待异步逻辑完成直接断言异步结果必然失败。解决方案是使用Awaitility或CyclicBarrier等待异步执行完成。Awaitility可以轮询等待条件成立超时兜底是很实用的测试工具。Test void shouldSendNotificationAsync() { notificationService.sendAsync(userId, Hello); await().atMost(5, TimeUnit.SECONDS) .untilAsserted(() - { NotificationRecord record notificationRepository.findByUserId(userId); assertNotNull(record); assertEquals(Hello, record.getContent()); }); }定时任务Quartz、Spring Scheduler在测试中最让人头疼的一点是它会被测试上下文带着一起启动导致测试跑着跑着突然有一个定时任务插入数据。我处理这类问题通常会在测试配置里把定时任务禁用掉要么通过ConditionalOnProperty控制调度器的装配要么直接用MockitoBean把Scheduler实例Mock掉。3. 集成测试场景从Mock到真实依赖3.1 用Testcontainers启动真实中间件很多测试一碰到数据库、Redis、MQ就头疼索性全用H2内存数据库Mock Redis凑合。但这样测试结果和生产环境的一致性很难保证特别是MySQL的SQL方言、二进制字段类型、事务隔离级别跟H2的差异可以在关键时刻给你“惊喜”。Testcontainers是解决这个问题的好工具。它可以在测试环境中通过Docker启动一个真实版本的MySQL、PostgreSQL、Redis、RabbitMQ测试跑完自动销毁容器。Testcontainers SpringBootTest class UserOrderIntegrationTest { Container static PostgreSQLContainer? postgres new PostgreSQLContainer(postgres:16-alpine) .withDatabaseName(testdb) .withUsername(test) .withPassword(test); DynamicPropertySource static void registerPgProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, postgres::getJdbcUrl); registry.add(spring.datasource.username, postgres::getUsername); registry.add(spring.datasource.password, postgres::getPassword); } Test void shouldSaveAndQueryUser() { // 业务逻辑测试 } }使用Testcontainers需要注意两点。第一测试机必须安装DockerCI环境也要有Docker守护进程这是硬性条件。第二容器每次启动需要几秒到十几秒不等所以不要把每个测试类都启动一个新容器而是尽量让测试类复用同一个容器实例。Testcontainers支持静态容器生命周期就是Container修饰static字段这样同一个测试类的所有用例共享一个容器但不同测试类之间还是可能各自启动容器可以在配置里通过固定容器名称、外部容器共享等方式进一步优化。3.2 第三方接口Mock的两种姿势在Service层或集成测试中经常需要调用第三方HTTP接口。这里有两种做法策略完全不同。第一种是用MockitoBean直接Mock掉第三方客户端的Bean。这种方式简单、快速适合在单元测试中隔离第三方依赖验证“我的代码在第三方返回成功/失败时的处理逻辑是否正确”。SpringBootTest class PaymentServiceTest { MockitoBean private PaymentGatewayClient paymentClient; Autowired private PaymentService paymentService; Test void shouldHandlePaymentSuccess() { when(paymentClient.charge(any())).thenReturn(new PaymentResponse(SUCCESS, TXN001)); PaymentResult result paymentService.pay(new PaymentRequest(U001, 100)); assertEquals(PaymentStatus.SUCCESS, result.getStatus()); } }第二种是用MockServer启动一个模拟HTTP服务让真实的客户端通过网络请求来访问。这种方式的优点在于整个HTTP调用链包括反序列化、超时重试、HTTP状态码处理都通过了真实验证。缺点是要额外引入MockServer依赖测试速度和第一种不是一个量级。两种方案怎么选我的判断是如果你要验证的是业务分支逻辑用MockitoBean就够了如果你想验证HTTP客户端的配置、拦截器、序列化这些底层细节就必须用MockServer。3.3 文件上传下载与大文件场景的测试文件上传下载是Spring Boot开发中的高频场景但测试这块很容易被忽视。很多项目上线前对文件接口的测试就是“自己在Postman里点一下”既不系统也没有回归价值。实际上Spring Boot测试里模拟文件上传非常方便用MockMultipartFile就能构造一个真实的上传请求。SpringBootTest AutoConfigureMockMvc class FileControllerTest { Autowired private MockMvc mockMvc; Test void shouldUploadFile() throws Exception { MockMultipartFile file new MockMultipartFile( file, report.pdf, MediaType.APPLICATION_PDF_VALUE, fake pdf content.getBytes(StandardCharsets.UTF_8) ); mockMvc.perform(multipart(/api/files) .file(file) .param(description, 月度报表)) .andExpect(status().isOk()) .andExpect(jsonPath($.fileName).value(report.pdf)); } }大文件场景有一点需要特别注意Spring MVC对文件上传默认有大小限制spring.servlet.multipart.max-file-size默认1MB测试大文件时如果直接构造几十MB的MockMultipartFile会导致请求失败。正确做法是在测试环境的配置中把限制调大或者用较小的分片来测试分片上传逻辑。对于分片上传、断点续传这类更复杂的场景建议把文件二进制内容用固定字节模式生成比如用固定循环的byte数组填充1MB、5MB、10MB既能控制体积又方便断言完整性。下载场景的测试思路类似通过MockMvc发起GET请求断言响应Content-Type、Content-Disposition和响应体内容。3.4 安全框架下的接口自测项目整合Spring Security或Shiro后接口测试会遇到另一个拦路虎你发送的请求如果没有登录态就会被过滤器拦截并返回401或302。测试中处理登录态的常见做法是使用WithMockUser或者自定义安全测试注解。WebMvcTest(UserController.class) class UserControllerSecurityTest { Autowired private MockMvc mockMvc; Test WithMockUser(username admin, roles {ADMIN}) void shouldAllowAdminToViewUserList() throws Exception { mockMvc.perform(get(/api/admin/users)) .andExpect(status().isOk()); } Test void shouldRejectAnonymousUser() throws Exception { mockMvc.perform(get(/api/admin/users)) .andExpect(status().isUnauthorized()); } }WithMockUser走的是Spring Security测试支持里内置的SecurityContext填充逻辑它不会真实走一遍认证流程而是直接把给定的用户名和角色塞进SecurityContext。如果你需要的是“走完整认证链路”的测试比如验证Token生成逻辑、验证密码加密那就需要用SpringBootTest RANDOM_PORT再通过RestTemplate或WebClient模拟真实登录。4. 高频踩坑问题与排查经验4.1 测试上下文加载慢、重复启动这是最普遍的性能问题。一个项目里如果有几十个测试类每个类都通过SpringBootTest启动完整的容器跑一遍全量测试可能就要十来分钟CI上根本跑不动。Spring Boot本身是有上下文缓存机制的对于配置相同的多个测试类它会复用同一个上下文。但上下文缓存复用有一个前提测试类上的相关配置要一致包括SpringBootTest的webEnvironment、ActiveProfiles的值、TestPropertySource的内容。只要你改了任何一个配置项Spring就认为上下文不兼容重新启动。提高速度的办法尽量使用测试切片WebMvcTest、DataJpaTest减少需要加载的Bean。多个测试类尽量保持相同的配置让上下文能被复用。少用DirtiesContext这个注解会强制关闭并重建上下文对性能影响显著。只在你明确需要清理内部状态时才使用。用AutoConfigureMockMvc配合SpringBootTest避免为了MockMvc而启动真实端口。4.2 Spring Boot版本过高导致不兼容这几年Spring Boot从2.x升级到3.x测试体系也跟着变了不少。最典型的是Spring Boot 3.x的包名从javax.servlet迁移到了jakarta.servlet部分注解和旧版测试代码直接编译不过。还有Spring Boot 3.2开始MockBean和SpyBean被标记为废弃官方推荐改用Spring Framework 6.1提供的MockitoBean和MockitoSpyBean。虽然当前版本用MockBean依然能工作但升级到新版本时这类替换是很常见的编译失败点。另一个版本相关问题是我在项目中实际踩过的Spring Boot版本升级后原本正常通过的测试开始报ClassNotFound或NoSuchMethodError。这种问题多半是测试依赖的版本没有随Boot版本同步升级比如JUnit 4切到JUnit 5、Mockito版本过旧、Testcontainers与Spring Boot集成模块版本不匹配。我的排查套路是——先看Spring Boot的BOMBill of Materials有没有管理对应依赖版本如果有从pom里删掉显式的版本号如果没有再对照官方文档推荐版本手动指定。4.3 循环依赖导致容器启动失败Spring Boot 2.6之后默认禁用了Bean的循环依赖。这意味着测试环境和线上环境一样一旦存在循环依赖启动容器就会直接失败。循环依赖的报错信息一般是The dependencies of some of the beans in the application context form a cycle解决思路有几种通过构造器注入重构代码从根本上拆掉环。如果没法立刻重构且确认是业务上可以接受的循环依赖可以在配置里通过spring.main.allow-circular-referencestrue临时放开。更推荐的做法是把相互依赖的Bean拆分成更细粒度的Bean或者利用Lazy延迟注入打破环。有一点要提醒循环依赖重构往往不是一次能完成的我的做法是先确认依赖关系优先消除那些容易被测试直接加载的Bean的循环引用比如Service和Mapper之间、两个Service之间。先让测试能跑起来后续再逐步优化。4.4 Transactional在测试中失效的场景上面提到过测试方法上的Transactional依赖Spring的代理机制。以下情况会导致事务回滚失效Spring Boot 3.x搭配JUnit 5如果测试类没有标注SpringBootTest而是直接使用JUnit的生命周期注解Transactional可能不会被识别。事务方法是非public的。Spring默认只对public方法做事务代理。测试方法里调用了同类的另一个方法并且那个方法上有事务注解。这是类内部调用问题事务切面不会拦到“this调用”。数据源类型不支持事务回滚比如某些NoSQL数据库或非标准数据源。排查这类问题时我一般先确认测试类上是否真的有SpringBootTest或对应的事务感知注解再检查方法访问级别最后检查数据库类型。还有一个很隐蔽的点如果你的测试里使用了AssertJ的软断言并且断言失败发生在事务提交之后那回滚自然就不生效了。4.5 端口冲突问题用RANDOM_PORT时一般不会冲突但用DEFINED_PORT比如固定8080就会出现端口被占用的情况。特别是在本机开发环境下如果你同时启动了一个应用实例、一个IDE缓存的老进程、一个Docker容器就会撞端口。比较稳妥的做法是本地测试统一用RANDOM_PORT避免硬编码端口。如果确实需要固定端口可以在测试配置里用server.port0让系统自动分配空闲端口同时在启动时通过ApplicationEnvironmentPreparedEvent监听器获取实际端口。4.6 测试顺序导致的状态互相污染JUnit默认的测试方法执行顺序是不确定的这本身没问题但一旦测试代码里有共享的静态状态、缓存、数据库数据就会产生顺序相关的假失败。比如A用例插了一条数据B用例也插同一条数据如果A先跑B可能重复插入而失败。解决方案每个测试方法尽量自包含数据使用BeforeEach清理环境。数据库类测试统一依赖事务回滚或者每次执行前用DELETE清空关键表。静态Mock状态MockitoStatic用完必须关闭否则会串到其他测试方法。4.7 常见问题速查表问题现象可能原因处理建议测试启动很慢全量加载SpringBootTest改用切片测试复用上下文找不到数据库驱动或SQL语法错误H2与MySQL方言差异使用Testcontainers跑真实数据库MockBean不生效注入的Bean不是容器中的真实Bean检查是否用成了new xxx()手动创建测试结果随机失败测试之间存在数据污染用事务回滚或BeforeEach清理数据端口被占用固定端口与本地进程冲突改用RANDOM_PORTSpring上下文一直重建测试配置不统一统一注解、profile和配置项环境变量在测试中读不到测试配置没加载用TestPropertySource或DynamicPropertySource5. 测试提速与团队落地经验5.1 全量测试与快速反馈的分层策略这一节想聊点工程实践层面的经验。我在团队里推行的测试策略并不复杂核心是“快和准”两件事。日常开发阶段Controller层用WebMvcTestService层用SpringBootTest数据层用DataSource或Testcontainers通过Maven或Gradle的命令只跑受影响的模块测试而不是每次全量跑。发版前CI上才会执行完整测试套件保证没有跨模块的破坏。这里有一个很容易被忽略的点测试代码也是代码也需要review。评审业务代码时我会顺便看一眼测试用例断言是否真实有价值。最典型的坏味道是只断言了状态码200却没有任何业务字段的校验这种测试对回归保护的意义非常有限。好的测试应该能回答“这个接口在什么输入下应该返回什么结果”这个问题。5.2 并行测试的思路与代价JUnit 5本身支持并行测试通过junit-platform.properties配置junit.jupiter.execution.parallel.enabledtrue junit.jupiter.execution.parallel.mode.defaultconcurrent junit.jupiter.execution.parallel.mode.classes.defaultconcurrent但并行测试不是免费的午餐。它要求测试之间必须完全隔离数据库、Redis、Mock状态、线程上下文全部不能共享。如果项目还没做到测试隔离强行开并行反而会引入大量间歇性失败。我的建议是分两步走先通过清理数据和事务回滚保证隔离性再考虑开启并行。并行度从2或4开始观察稳定运行一段时间后再逐步提高。5.3 把测试跑进CI/CD流程我还想强调一个工程习惯把测试纳入CI流程并且让测试失败直接阻断发布。这听起来是常识但很多团队实际并没做到。常见的情况是CI只执行“编译成功”测试被单独放到一个nightly job里或者测试任务失败时CI仍以成功结束。正确做法是让测试任务成为发布管道的前置检查且失败即终止。同时测试报告要能被团队直接查看包括覆盖率、失败用例的堆栈、运行耗时等。没有这些基础设施测试写再多也只是“为了写而写”。5.4 我个人的几条测试习惯最后分享几个我个人坚持了很久的习惯不一定适合所有团队但对我自己的项目质量和维护效率有实打实的帮助。第一测试命名尽量描述业务行为而不是实现细节。比如shouldRefundOrderWhenPaymentCanceled这样的命名比testCancelOrder好得多。测试挂掉时看日志就能立刻明白哪个业务场景出了问题。第二每个测试方法只验证一个核心行为。如果一个方法里既要验证正常返回又要验证异常分支还要验证边界条件我通常会拆成三个方法。测试用例之间应当互相独立一个用例挂了不影响排查其他用例。第三对事务相关的业务逻辑一定单独验证提交后的状态而不是只依赖测试结束时的自动回滚。自动回滚能保证测试环境干净但它也让开发者容易忽略事务提交前后数据可见性的差异。只有验证提交后的真实状态才会发现哪些事务边界设计有问题。第四测试代码和业务代码一样需要维护。每次重构业务逻辑时顺手重构测试用例让测试代码维持在一个“随时能跑跑完能信”的状态。以上这些经验都是我在实际项目里一步步踩出来的。SpringBoot Test这套体系覆盖面很广从简单的单元测试到复杂的集成测试都能找到对应的解决方案。如果你正在为自己的Spring Boot项目搭建测试体系可以拿这篇文章里的思路做参考先把“能跑通、能断言、能回滚”三层基础打好再逐步叠加切片测试、Testcontainers、并行执行这些高级能力。测试这件事投入越早回报越明显。