news 2026/9/9 3:12:54

SpringBoot测试实战:从@SpringBootTest到Testcontainers

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot测试实战:从@SpringBootTest到Testcontainers

1. 测试不是后端开发的备选项:先算一笔成本账

我在好几家公司待过,见过太多项目上线前才发现接口字段对不上、数据库时间少了八小时、老功能被新需求改挂的场景。团队的第一反应往往是"加个监控吧""下次注意点",但下一次照样翻车。原因很简单:没人愿意写测试,因为总觉得测试是"额外工作量"。但如果你真正把 SpringBoot 的测试体系用起来,会发现它要解决的根本不是"加不加工作量"的问题,而是"你改代码的时候敢不敢直接部署"的问题。

1.1 一个没写测试的支付回调 Bug,让我后悔了很久

说个真实例子。之前做一个支付系统,回调接口要做三件事:验签、解析订单号、更新订单状态。上线当天发现,当订单金额为 0 的时候,数据库里金额字段被写成了 NULL,而前端判断amount == null直接报空指针。查了半天,是团队里另一位同事在改需求时把"不传金额就默认为 0"的判断删了。如果当时有一个针对回调接口的测试,哪怕只覆盖三个分支,这个问题在mvn test阶段就能拦住,而不是等用户投诉之后再去查日志。

这类问题在 SpringBoot 项目里太常见了。Controller 的返回值变了、MyBatis 的 XML 里字段名对不上、Redis 序列化方式改了,这些靠"人工点一遍页面"是很难全部发现的。而 SpringBoot Test 这套东西,就是让你用 JUnit 写一批自动化的验证程序,跑一次mvn test就能把核心逻辑全部过一遍。

1.2 测试金字塔放在 Spring Boot 生态里长什么样

测试金字塔的概念不用我多说:底层是大量快速、廉价的单元测试,中间是服务层的集成测试,顶层是少而全的端到端测试。SpringBoot 对这三层都有原生支持:

层级技术选型启动成本典型场景
单元测试JUnit 5 + Mockito毫秒级Service 内纯逻辑、工具类
切片测试@WebMvcTest / @DataJpaTest秒级Controller、Repository
集成测试@SpringBootTest + Testcontainers十秒级跨模块链路、外部中间件

很多人的误区是一上来就@SpringBootTest往所有测试类上贴,导致启动一次上下文要十几秒,几百个测试跑下来花费半小时,然后得出结论"测试太慢了,不适合我们项目"。其实这恰恰不是框架的问题,而是没有做测试分层。搞懂 SpringBoot 到底提供了哪些测试能力,按层级去用,就能既保证测试速度,又保证覆盖质量。

2. @SpringBootTest到底干了什么:注解背后的真实装配逻辑

@SpringBootTest是 SpringBoot 测试体系的地基。我第一次用的时候,以为它只是"帮你把项目跑起来",但直到自己排查了一个诡异问题才真正搞明白它背后做了什么:它会根据你项目的主配置类(@SpringBootApplication标注的类)去启动整个 Spring 应用上下文,把 Controller、Service、Repository、消息队列消费者,甚至定时任务全部加载进来。

2.1 它加载了什么,不加载什么

默认情况下,@SpringBootTest没有指定classes属性,它会从当前测试类所在的包开始向上查找@SpringBootConfiguration注解,找到之后就把这个配置类当作启动入口,然后走一遍和main方法几乎相同的自动装配流程。也就是说,你在application.yml里配置的数据源、Redis、消息队列,它都会去尝试连接。

这个机制带来一个常见问题:当你本地的 MySQL 没启动或者配置中心连不上时,测试直接就Failed to load ApplicationContext。我见过很多新手在这里卡住,然后觉得"测试真难用"。其实这不是 bug,而是你还没有告诉 Spring 测试环境该怎么连这些外部组件。后面我会讲怎么用 Testcontainers 和@MockBean来解这个问题。

@SpringBootTest还内置了一个非常有用的特性:默认会查找src/test/resources下的application.ymlapplication-test.yml。如果你的测试配置和正式配置差异很大,可以在测试资源目录里单独写一份,通过@ActiveProfiles("test")指定使用哪个 profile。我个人的习惯是测试环境固定一个testprofile,里面把日志调成 WARN、把缓存换成本地实现、把消息队列消费关掉,这样既能跑通链路,又不会被外部依赖拖死。

2.2 webEnvironment 四种模式怎么选

@SpringBootTestwebEnvironment属性默认是MOCK,这个默认值其实非常关键。它表示测试时不会启动真实的 HTTP 服务器,而是通过 Spring 的MockHttpServletRequest模拟 HTTP 请求。这个时候要测 Controller,需要配合MockMvc手动构造请求。下面给个表格,把四种模式一次说清楚:

webEnvironment 取值是否启动真实端口适用场景
MOCK否,使用 Servlet 模拟环境配合 MockMvc 做 Controller 层测试
RANDOM_PORT是,随机可用端口需要真实 HTTP 请求的集成测试,配合 TestRestTemplate
DEFINED_PORT是,使用配置端口 8080基本不推荐,容易端口冲突
NONE否,只加载上下文不涉及 Web 层的 Service/Repository 测试

RANDOM_PORT 模式是我做微服务联调测试时最常用的。它启动一个真实的内嵌 Tomcat,监听随机端口,然后你可以用@LocalServerPort把这个端口注入到测试类里,再配合TestRestTemplate发送真实 HTTP 请求。这种方式连过滤器、拦截器、异常处理器都会真实走一遍,比 MockMvc 更贴近生产。

2.3 测试实例生命周期:JUnit 5 和 JUnit 4 的差异

SpringBoot 2.2 之后默认使用 JUnit 5,测试类默认是"每个方法新建一个实例"的 PER_METHOD 生命周期。这在绝大多数情况下是没问题的,但如果你在测试类里定义了成员变量来缓存某些数据,就要小心了,因为每次执行测试方法之前 Spring 都会重新装配一次上下文中的 Bean,但测试类本身会被重新实例化,之前存进去的成员变量会丢失。

我踩过一个坑:在测试类里放了一个List<String>用来记录接口返回的错误码,结果跑第二个测试方法时列表是空的。原因就是 JUnit 5 的 PER_METHOD 生命周期。后来我改用@TestInstance(TestInstance.Lifecycle.PER_CLASS)配合@TestMethodOrder,才保证了实例复用和有序执行。当然,这种做法有隐患,测试之间会共享状态,所以只适合确实需要复用场景的测试,能用局部变量的地方尽量不要依赖成员变量。

3. 别一竿子把所有测试都拉成完整上下文:切片测试的边界

说道测试慢,90% 的原因是每个测试类都用了@SpringBootTest,每个类都加载一遍完整应用上下文。SpringBoot 其实提供了一套叫"切片测试"(Slice Test)的机制,它只加载你当前测试目标所需要的那一部分 Bean。这才是让测试跑得快、跑得稳的关键。

3.1 @WebMvcTest:只把 Controller 层拉起来

@WebMvcTest只加载 Web 层相关的内容:@Controller@ControllerAdviceFilterHandlerInterceptorJackson配置等。它不会加载 Service、Repository,也不会连数据库。这意味着测试 Controller 时,你需要把依赖的 Service 用@MockBean打桩。

有段时间我们项目里有个比较复杂的查询接口,Controller 层要做参数校验、分页封装、异常转换。我写了一个@WebMvcTest测试,把所有 Service 依赖全部 mock 掉,只测 Controller 的入参校验逻辑:

@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnBadRequestWhenUsernameIsBlank() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"\"}")) .andExpect(MockMvcResultMatchers.status().isBadRequest()) .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("用户名不能为空")); } }

这种测试跑起来非常快,几十个测试类一起跑也就几秒钟。它唯一的缺点是脱离了真实 Service 实现,所以它只能验证"参数传错了会返回什么",不能验证"数据查出来之后能不能正确拼装响应"。这两件事要分开测,后者交给 Service 层的单元测试和集成测试。

3.2 @DataJpaTest:只测数据库交互

和数据层相关的切片是@DataJpaTest。它扫描@Entity和 Spring Data JPA 的 Repository 接口,默认用嵌入式数据库(比如 H2)来代替你配置的真实数据库,而且每个测试方法默认开启事务,方法执行完自动回滚。这意味着你可以在测试里随便 insert、update、delete,不需要担心污染真实数据。

但这里有个大坑:如果你的项目用了 MyBatis,@DataJpaTest就不好使了,因为它只支持 Spring Data JPA 的自动装配。MyBatis 项目要测 Mapper,我一般直接用@MybatisTest(mybatis-spring-boot-starter-test 提供)或者干脆走@SpringBootTest+ Testcontainers。另外,就算用 JPA,如果你写了原生的@QuerySQL,里面用了 MySQL 特有的函数(比如DATE_FORMAT),H2 可能会不认识,跑测试直接报语法错误。这种情况下要么换 H2 的兼容模式,要么就直接上 Testcontainers 跑真实 MySQL。

3.3 切片测试的边界和 @MockBean 的正确使用姿势

切片测试看似简单,边界其实很容易踩过头。@WebMvcTest里如果你用了@EnableFeignClients或者自定义的WebMvcConfigurer,有可能加载失败,因为 Feign 客户端不在被加载的 Bean 集合里。解决方案一般是在测试配置里排除这些自动配置:

@WebMvcTest(controllers = UserController.class, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = FeignClientConfig.class))

@MockBean也有讲究。它可以把一个 Bean 替换成 Mockito 的 mock 对象。注意它替换的是上下文里的 Bean,所以如果两个测试类对同一个 Bean 打桩的行为不一致,Spring 会为每个组合缓存独立的上下文。滥用@MockBean会导致上下文数量爆炸,拖慢整个测试套件。我见过一个项目测试类越来越多,上下文缓存了几十个,跑一次全量测试光启动上下文就花了好几分钟。后来我们约定:能用构造器注入的地方尽量用构造器,能在一个测试类里集中打桩的就不要拆成多个类。

4. 外部依赖怎么处理:Mockito 模拟和 Testcontainers 真实环境

写完单元测试和切片测试,还会剩下一批真正需要外部依赖的集成测试。比如验证 Redis 缓存逻辑是否生效、Kafka 消费者消费消息后是否正确落库、MySQL 事务回滚是否符合预期。这些场景有两个主流方案:Mockito 把外部客户端 mock 掉,或者用 Testcontainers 把真实中间件拉起来。两者不是互斥关系,而是互补关系。

4.1 Mockito 和 Spring 上下文是怎么协同的

Mockito 是一个独立的 Java mock 框架,它和 Spring 的协同一般通过@MockBean@SpyBean完成。@MockBean会创建一个 mock 对象,替换掉容器中的真实 Bean,然后通过Mockito.verify()when(...).thenReturn(...)设置行为和校验调用。

我举一个实际工作中的例子。下单成功后我们要发一条 MQ 消息,但测试环境没有部署消息队列,所以我用@MockBeanMessageProducermock 掉:

@SpringBootTest @ActiveProfiles("test") class OrderServiceTest { @Autowired private OrderService orderService; @MockBean private MessageProducer messageProducer; @Test void shouldSendMessageAfterOrderCreated() { Order order = new Order(); order.setOrderNo("20240501001"); orderService.createOrder(order); verify(messageProducer, times(1)).send(any(OrderCreatedEvent.class)); } }

这样测试就跑得很快,不需要真的 MQ。但是请注意,mock 掉的 Bean 不会再执行真实逻辑,所以它验证不了"消息格式是否正确"。如果你要测的是序列化之后的消息体,建议还是用真实 MQ 的 Testcontainers 镜像做一次真正的收发。

4.2 Testcontainers:用真实中间件测试,而不是模拟

Testcontainers 是一个很实用但很多人还没用起来的库。它能在测试启动时用 Docker 拉起一个 MySQL、Redis、Kafka 容器,测试结束自动销毁,保证测试环境干净且和线上行为一致。SpringBoot 3.1 之后官方支持了@ServiceConnection注解,可以自动把 Testcontainers 启动的容器配置注入到 Spring 环境里,省去手动拼 JDBC URL 的麻烦。

一个常见的 MySQL 集成测试配置长这样:

@Testcontainers @SpringBootTest @ActiveProfiles("test") class UserRepositoryIntegrationTest { @Container @ServiceConnection static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @Autowired private UserRepository userRepository; @Test void shouldSaveAndFindUser() { User user = new User(); user.setUsername("jack"); userRepository.save(user); Optional<User> found = userRepository.findByUsername("jack"); assertThat(found).isPresent(); assertThat(found.get().getUsername()).isEqualTo("jack"); } }

@ServiceConnection是 Spring Boot 3.1 加入的,我个人非常推荐。以前我们需要手动从容器里取端口再写到SpringApplication.setDefaultProperties里,写起来很啰嗦。现在这个注解会自动把容器连接信息绑定到DataSourceProperties,后面的任何 JPA 或 MyBatis 配置都不需要改。

4.3 本地没有 Docker 怎么办

Testcontainers 依赖 Docker,但 CI 环境或者部分开发机可能无法运行 Docker。这里我有两个经验:

第一,把需要外部中间件的测试用 JUnit 的@Tag标记分类,比如@Tag("integration"),然后在pom.xml的 surefire 插件里用excludedGroups排除这些测试,保证mvn test在没有 Docker 的环境下也能跑。集成测试放在单独的 profile 里执行。

第二,用 Maven 的 profile 区分"本地快速测试"和"完整集成测试"。本地开发时只跑非集成测试,提交 CI 后再单独起一个 job 跑完整的集成测试。这样既照顾了开发体验,又没有回避真实环境验证。

5. 集成测试实战:从 MockMvc 到数据库的完整闭环

理论讲再多,不如完整走一遍。下面我用一个最常见的"用户注册"功能,展示从 Controller 到 Service 到 Repository 的测试怎么写,以及每个阶段应该断言什么。

5.1 先准备被测代码

假设要测的功能如下:

@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping public ResponseEntity<UserResponse> register(@Valid @RequestBody RegisterRequest request) { UserResponse response = userService.register(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } }

Service 里做用户名唯一性校验、密码加密、保存用户:

@Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public UserResponse register(RegisterRequest request) { if (userRepository.existsByUsername(request.getUsername())) { throw new BusinessException("用户名已存在"); } User user = new User(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setCreatedAt(LocalDateTime.now()); userRepository.save(user); return UserResponse.from(user); } }

5.2 第一层:@WebMvcTest 验证参数校验和响应结构

写 Controller 层测试时,我不关心 Service 里的用户名重复判断是否正确,只关心几件事:参数校验错误时是否返回 400、Service 正常返回时响应体结构是否对、异常抛出来之后全局异常处理器是否能转成统一错误格式。

@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnCreatedWhenRegisterSucceeds() throws Exception { when(userService.register(any(RegisterRequest.class))) .thenReturn(new UserResponse(1L, "jack", LocalDateTime.now())); mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"jack\",\"password\":\"123456\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.username").value("jack")) .andExpect(jsonPath("$.id").value(1)); } @Test void shouldReturnBadRequestWhenPasswordTooShort() throws Exception { mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"jack\",\"password\":\"123\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").exists()); } }

这里的要点是jsonPath断言,它用的是 Jayway JsonPath 表达式。$.username表示根节点下的 username 字段。在实际项目中,我还喜欢把响应中的时间字段单独断言,因为 LocalDateTime 默认序列化成数组或者 ISO 字符串,不同配置结果不一样,不写清楚的话测试非常容易飘。

5.3 第二层:@SpringBootTest 打通 Service 和真实数据库

第一层测试验证的是"管道",但用户是不是真的存进去了,要靠集成测试。这里我用@SpringBootTest配合 H2(或者 Testcontainers MySQL),直接调用 Service,验证数据库里的状态:

@SpringBootTest @ActiveProfiles("test") @Transactional class UserServiceIntegrationTest { @Autowired private UserService userService; @Autowired private UserRepository userRepository; @Test void shouldSaveUserToDatabase() { RegisterRequest request = new RegisterRequest("alice", "password123"); UserResponse response = userService.register(request); assertThat(response.getId()).isNotNull(); User saved = userRepository.findByUsername("alice").orElseThrow(); assertThat(saved.getPassword()).isNotEqualTo("password123"); assertThat(saved.getCreatedAt()).isNotNull(); } @Test void shouldRejectDuplicateUsername() { RegisterRequest first = new RegisterRequest("bob", "password123"); userService.register(first); RegisterRequest duplicate = new RegisterRequest("bob", "another123"); assertThatThrownBy(() -> userService.register(duplicate)) .isInstanceOf(BusinessException.class) .hasMessageContaining("用户名已存在"); } }

注意我加了@Transactional,这样每个测试方法跑完自动回滚,测试之间互不影响。这里要说明的是:@Transactional只对事务内通过 Spring 代理调用的方法生效,如果 Service 方法内部自己开了新事务(比如@Transactional(propagation = Propagation.REQUIRES_NEW)),回滚就无效了,测试数据可能残留。遇到这种情况,我一般改用 Testcontainers +@BeforeEach里手动清理表数据,或者给测试容器用独立的库,跑完直接销毁。

5.4 第三层:真实 HTTP 调用,验证过滤器、拦截器和序列化

如果项目里有登录校验过滤器、请求日志拦截器、全局异常处理器,MockMvc 的 MOCK 环境也能覆盖大部分场景,但总有些边角要真实端口才靠谱。比如你配置了@ControllerAdvice处理特定异常,而@WebMvcTest时它可能因为自动配置的加载范围问题没有被注册,这时就需要 RANDOM_PORT + TestRestTemplate:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class UserApiE2ETest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void shouldRegisterThroughRealHttp() { RegisterRequest request = new RegisterRequest("e2e_user", "password123"); ResponseEntity<UserResponse> response = restTemplate.postForEntity("http://localhost:" + port + "/api/users", request, UserResponse.class); assertThat(response.getStatusCode().value()).isEqualTo(201); assertThat(response.getBody().getUsername()).isEqualTo("e2e_user"); } }

这种测试跑起来比 MockMvc 慢,但是最接近真实。我把它们控制在核心链路场景,比如登录、下单、支付回调,而不是所有接口都写一遍,否则 CI 时间会变得不可接受。

6. 我在真实项目里踩过的测试坑,以及最终的修复方式

SpringBoot 测试的资料不少,但大多数教程只教你"怎么写",没教你"踩坑了怎么排查"。我把这几年实际遇到的、网上搜不到现成答案的几个问题列出来,每一个都查了很久才定位到根因。

6.1 上下文缓存导致的两个测试类互相污染

Spring 的TestContext框架默认会缓存已加载的应用上下文,key 由配置类、激活的 profile、webEnvironment 等组合而成。看起来是个优化,实际上会引入一个隐蔽问题:如果测试 A 修改了某个 Bean 的内部状态,而测试 B 复用了同一个上下文,那么 B 拿到的 Bean 状态已经被污染了。

我之前遇到过一个问题:测试类 A 往 Redis 里写了一个 key,类 B 的测试用例本来期望 key 不存在,结果第一次跑失败,单独跑一个类又是好的。排查后发现是@SpringBootTest默认上下文缓存共享导致的。修复方式有两个:一是每个测试类尽量自己清理外部状态(比如@AfterEach清空 Redis);二是如果需要完全隔离,用@DirtiesContext标注测试类,强制 Spring 在测试结束后关闭并清除上下文缓存。

但要慎用@DirtiesContext,因为它会让每个标记类都重新启动上下文,直接影响测试速度。我的策略是:默认不标记,只有当测试确实会修改容器内全局状态时才加。

6.2 随机端口模式下,测试并发执行抢端口

做了 RANDOM_PORT 之后,端口虽然是随机的,但如果你用 Maven Surefire 默认的 forkCount 配置,多个测试类并发执行时,有可能出现同一个 JVM 里两个上下文同时启动两个 Tomcat,最终端口冲突。尤其是用了@DirtiesContext之后,上下文销毁和重建的间隙更容易出问题。

我的修复经验是:不要人为调大 surefire 的 forkCount,默认的单进程执行对于 SpringBoot 测试来说反而是最稳定的。如果你要缩短测试时间,优先考虑减少不必要的@SpringBootTest,用切片测试来替换,而不是靠并发执行硬扛。

Surefire 里还可以显式配置禁用重跑测试类的并发:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <parallel>none</parallel> <forkCount>1</forkCount> <reuseForks>true</reuseForks> </configuration> </plugin>

6.3 测试资源配置文件覆盖不完整导致连了生产库

这是我最想强调的一个坑。src/test/resources下的application.yml会覆盖src/main/resources下的同名配置,但它不是"替换"整个文件,而是按 key 合并的。也就是说,如果主配置里写了spring.datasource.url指向生产数据库,测试配置里只写了spring.datasource.username,那么 URL 仍然会从主配置里继承。

后果非常严重。有人不小心在本地跑测试,因为测试配置里漏写了一项,结果直连了生产库,还执行了清表操作。我现在的做法是:测试配置里把spring.datasource.urlspring.datasource.usernamespring.datasource.passwordspring.redis.*所有关键连接信息全部显式覆盖一遍,然后用@ActiveProfiles("test")强制激活,并在 CI 环境变量里注入一个SPRING_PROFILES_ACTIVE=test做兜底。另外,正规一点的做法是在主配置里把生产库密码放到环境变量或配置中心,本地根本没有生产库密码,自然连不上。

6.4 时间字段断言总是飘,问题出在序列化配置

在接口测试里最烦的问题之一就是 LocalDateTime 的断言。一开始我在jsonPath里写$.createdAt等于某个固定时间,但实际返回的是"2024-05-01T10:20:30"或者"2024-05-01 10:20:30",取决于你是否配置了 Jackson 的JavaTimeModuleWRITE_DATES_AS_TIMESTAMPS

后来我总结了一个稳妥做法:断言时间字段时,不要断言全等,而是用 JsonPath 的正则匹配或者反序列化成对象后再比较。比如:

.andExpect(jsonPath("$.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")))

如果测试场景需要精确比较时间,我倾向于在测试里先定义期望时间,然后调用被测代码,再取出数据库里的实际值,用AssertJisEqualToIgnoringNanos比较,避免毫秒和纳秒带来的误差。

6.5 测试代码里的"魔法值"和过度 mocking

最后一个坑不算技术问题,是工程习惯。很多测试代码里直接写死了一堆字符串和数字,比如用户名固定"test"、密码固定"123456",这些魔法值一旦业务含义变了,测试就失去了可读性。我在 review 代码时,遇到这种测试通常会建议抽成常量,或者用一个小工具类生成随机数据。

另外,过度 mocking 也非常常见。一个 Service 的方法依赖了 A、B、C 三个对象,你全部 mock 掉,然后只测了方法内部的 if-else 分支。这种测试看起来很安全,实际上和业务逻辑已经脱节,一旦 A、B、C 之间的协作关系变了,测试照样绿,但代码已经坏了。个人建议是:如果某个测试需要 mock 四个以上协作者,要么重构被测类的粒度,要么直接上真正的集成测试,让协作者用真实实现跑起来。

写在最后的个人习惯

说了这么多,其实 SpringBoot Test 的核心就一句话:让测试快、稳、准。快依赖切片和合理分层,稳依赖隔离和配置正确,准依赖断言别糊弄。我现在的团队把测试分三类:本地跑得快的单元测试和切片测试、CI 里跑的真实中间件集成测试、发布前跑的核心链路冒烟测试,每次提交代码前一条mvn test已经成了肌肉记忆。

如果这个项目你刚接手、历史代码没测试,我的建议是先别急着给旧代码补全量测试,挑三个最核心的链路先补:登录鉴权、下单支付、数据导出。这三个链路跑通了,后续重构的底气和安全边界也就有了。其他的地方,边改边补,比一次性补完更现实也更有效。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 3:10:50

Azure与Microsoft SVG图标集合:云架构图与技术文档的矢量图标解决方案

简介&#xff1a;面向云计算架构师、前端开发与视觉设计人员的SVG图标合集&#xff0c;核心覆盖Azure相关服务与组件图标&#xff0c;同时收录多种技术品牌标识、徽章及抽象通用符号&#xff0c;可用于系统架构图绘制、技术文档配图、PPT演示或Web界面示意等场景。所有文件均经…

作者头像 李华
网站建设 2026/9/9 3:10:34

cpr 1.3.0库文件编译与集成:从CMake构建到跨平台部署

简介&#xff1a;cpr-1.3.0 是 C 开发者常用的 HTTP 网络请求库&#xff0c;API 风格简洁&#xff0c;类似 Python requests&#xff0c;适合编写网络爬虫、访问 REST API 以及进行服务端接口调试。面向 VS2015 环境&#xff0c;预编译库文件包省去自行编译与配置依赖的繁琐过程…

作者头像 李华
网站建设 2026/9/9 3:08:57

JS核心语法30分钟速成:变量、函数、异步与模块化实战指南

如果你正准备入门前端&#xff0c;或者刚学完 HTML 和 CSS&#xff0c;想迈出 JavaScript 这一步&#xff0c;那这篇内容就是为你准备的。先纠正一个常见认知&#xff1a;JavaScript 不是“很简单”的语言。它简单在语法层面&#xff0c;复杂在运行模型。很多人学了三个月&…

作者头像 李华
网站建设 2026/9/9 3:08:03

树莓派Pico低功耗实战:睡眠模式与GPIO唤醒全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 3:07:47

DS9解压即用:天文FITS图像可视化与Region标注实战指南

简介&#xff1a;DS9是一款面向天文学与科学数据处理的可视化工具&#xff0c;主要用来查看和分析FITS图像、光谱、二进制表等多维数据&#xff0c;支持多帧缓冲区、区域操作和多尺度算法&#xff0c;适合科研人员和天文爱好者快速开展数据探索。该7z压缩包共88个文件&#xff…

作者头像 李华
网站建设 2026/9/9 3:07:37

基于SSM+Android的校园交流APP设计与实现全解析

又到一年毕设季&#xff0c;每年都能看到一群人在校园交流类App这个题目上反复纠结。说实话&#xff0c;基于SSMAndroid做校园交流APP&#xff0c;属于那种特别经典的计算机专业毕业设计选题&#xff1a;技术栈主流、功能拓展空间大、前后端闭环完整&#xff0c;而且无论做论坛…

作者头像 李华