资讯动态

SpringBoot单元测试实战:从工具链到分层测试与踩坑指南

发布时间:2026/9/8 11:36:59 来源:尧图企业网站定制
1. 为什么SpringBoot项目的单元测试经常形同虚设接手过几个SpringBoot项目之后我发现一个特别普遍的现象代码仓库里存在test目录但里面要么是空的要么只有一个自动生成的SpringBootApplicationTests跑一下只是验证了Spring容器能正常启动没有任何实际断言。这种事情见多了以后我反而觉得这不能全怪开发团队——大多数人不是不想写测试而是没有建立起对单元测试的正确理解再加上前期没有好的示例做参照写起来阻力就会很大。1.1 从能跑到能测很多项目卡在哪一步很多团队排查问题的原始方式是重启一下看日志再手动点一遍页面这套流程在工作节奏快的时候确实管用。一旦项目规模变大WebMvc、MyBatis、Redis、MQ都堆进来了你改一个Service层的数据校验逻辑影响的可能是三个Controller和两个定时任务。这种状态下想靠人肉回归来保证质量几乎是不可能的。这种情况下的单元测试本质上就是一个行为契约。它锁定的不是某一行代码写了什么而是给定这样的输入这个方法应该返回什么或者应该调用什么依赖、不应该调用什么依赖。我在项目里见过最典型的一个例子有人改了订单状态流转的代码把已支付改成了已完成结果下单流程里一个判断订单状态的地方炸了。如果有针对状态流转的单元测试这一下就能在编译阶段暴露出来——跑了测试红了一片谁改的谁心里有数。另外SpringBoot本身对测试的重视程度是很高的它提供了spring-boot-starter-test你只要在pom里加一行依赖JUnit、Mockito、AssertJ这些工具全都齐了。换句话说基础条件已经完全具备写测试最大的障碍其实不在于工具而在于思维习惯——你能不能把一个功能点拆成独立可验证的最小单元。一旦跨过这个坎后面越写越顺。1.2 单元测试到底在保护什么有时候会听到这样一种声音我写的是CRUD代码这么简单哪需要测试我也曾经这么想过直到自己在生产环境栽过跟头——一个极端边界情况没有被覆盖后来数据异常了排查了一整天最后发现就是一行和的区别。单元测试保护的是行为边界。业务代码里最容易出Bug的地方不是那些主流程而是异常路径、空值判断、状态非法这些边边角角。拿用户注册来说主流程就是插入一条用户记录看起来确实没什么好测的。但用户名重复怎么办邮箱格式不合法怎么办事务回滚是否彻底这些才是单元测试应该重点关注的部分。而且单元测试是一种低成本的试错方式。集成测试要启动整个Spring容器连数据库、连Redis跑一遍要好几分钟单元测试的运行时间通常以毫秒计算。理想的项目测试策略应该是底层单元测试数量庞大运行速度极快上层集成测试数量少而精。先把单元测试这一层做实项目的稳定性才会有一个坚实的基础。2. 测试环境搭建与工具链选型JUnit 5、Mockito、AssertJ三件套要讲SpringBoot单元测试第一步肯定是把依赖先弄对。这里我见过很多新手在换版本或者换构建工具之后踩坑所以我先从spring-boot-starter-test这个依赖开始说起。2.1 spring-boot-starter-test里到底有什么先看一眼Maven依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency这行代码加进去之后你的test环境下就有了下面这些关键组件组件职责我的使用建议JUnit 5测试框架核心提供Test、Extension等注解主力框架跑测试靠它Mockito创建Mock对象定义依赖行为验证调用次数隔离依赖的唯一神器AssertJ流式断言库提供assertThat(...)链式校验让断言清晰可读强烈推荐Hamcrest老牌匹配器库JUnit自带集成用得少但偶尔配合MockMvc.andExpect用得上JSONassert比对JSON结构差异写接口测试时偶尔能用到JsonPath从JSON响应中提取字段值配MockMvc写jsonPath断言必备Spring Test Context缓存Spring上下文、事务回滚等测试支持集成测试的地基上面这些组件里日常最常用的就是JUnit 5、Mockito和AssertJ三个。我的习惯是断言一律用AssertJ的assertThat它比JUnit自带的assertEquals(xxx, actual)强在两点一是API设计更自然读代码像在读一句话二是失败时的错误信息非常详细直接告诉你左边期望是什么、右边实际是什么一眼就能看出差异在哪。还有一个小细节Spring Boot 2.2之后默认引入的是JUnit 5如果项目是基于Spring Boot 2.1或更早版本迁移过来的要留意是否还残留着JUnit 4的Test注解org.junit.Test。很多人迁移之后发现测试跑不起来查了半天才发现是导错了包。2.2 为什么我不用TestNG或者说什么场景下才考虑它有一些团队或者面试中会提到项目单元测试集成TestNG这个方案。TestNG作为一个成熟的测试框架它的并行执行、dependsOnMethods方法依赖、DataProvider数据驱动这些功能确实很强。但我的看法是如果你的项目是标准SpringBoot项目、也没有特殊的历史包袱直接用默认的JUnit 5就好。原因很直接SpringBoot官方对JUnit 5的集成是最顺畅的ExtendWith(SpringExtension.class)、DataJpaTest、WebMvcTest这些测试切片注解都是围绕JUnit 5设计的。Spring和TestNG配合虽然也能跑通但在SpringBoot的自动配置和测试切片方面文档和社区案例明显少一大截出了问题排查成本会高很多。TestNG真正发挥优势的场景是什么呢我理解是那些需要从外部数据源比如Excel、数据库批量加载数据来跑大量数据驱动的测试或者对测试执行顺序有硬性依赖的复杂集成测试。对于我们日常写业务代码的单元测试来说JUnit 5的ParameterizedTest已经能覆盖大部分数据驱动的需求没必要为了某个单独的功能点去切换整个框架。2.3 版本兼容别小看这一个配置项在引入spring-boot-starter-test之前先确认一下项目的Spring Boot版本。Spring Boot 2.2到2.7之间的版本对应的是JUnit 5.5到5.8到了Spring Boot 3.x之后对应的是JUnit 5.9/5.10同时Java版本要求也变成了Java 17。如果项目还在用JDK 8就老老实实停留在Spring Boot 2.7.x不要硬升。另外要注意一个非常经典的坑spring-boot-starter-test在Spring Boot 2.4之前的版本里默认包含了Mockito的核心依赖但如果你的项目里有其他测试框架比如Cucumber可能会把Mockito的依赖覆盖掉造成MockitoExtension找不到。解决办法就是在pom里显示声明你需要的测试依赖版本不依赖starter的传递依赖来兜底。3. 分层测试实战从Service到Repository再到Controller环境搭建好之后真正核心的问题就是怎么测。一个典型的SpringBoot项目会分成Controller、Service、Repository三层三层代码的关注点不一样对应的测试策略也不一样。我强烈建议一个原则能测Service层就不去启动整个Web容器能Mock掉底层依赖就绝不连真实数据库。下面按分层一个个说。3.1 Service层测试用Mockito把依赖隔离开Service层是业务逻辑最集中的地方也是单元测试性价比最高的位置。测试Service的最佳方式是用Mockito把它的依赖比如Repository、外部接口客户端全部替换成虚拟对象这样我们可以只关注Service方法本身的行为是否正确而不用关心它依赖的东西靠不靠谱。先看一个实际例子一个标准的用户服务Service RequiredArgsConstructor public class UserService { private final UserRepository userRepository; public User getUserByUsername(String username) { return userRepository.findByUsername(username) .orElseThrow(() - new UserNotFoundException(用户不存在: username)); } }对应的单元测试ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserRepository userRepository; InjectMocks private UserService userService; Test void shouldReturnUserWhenUsernameExists() { // given User expected new User(); expected.setUsername(zhangsan); when(userRepository.findByUsername(zhangsan)) .thenReturn(Optional.of(expected)); // when User actual userService.getUserByUsername(zhangsan); // then assertThat(actual).isNotNull(); assertThat(actual.getUsername()).isEqualTo(zhangsan); verify(userRepository, times(1)).findByUsername(zhangsan); } Test void shouldThrowExceptionWhenUsernameNotFound() { // given when(userRepository.findByUsername(nobody)) .thenReturn(Optional.empty()); // when then assertThatThrownBy(() - userService.getUserByUsername(nobody)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining(不存在); } }这个测试里有几个细节值得说明ExtendWith(MockitoExtension.class)这是JUnit 5和Mockito集成的入口没有这个注解Mock、InjectMocks就不会生效。Mock和InjectMocksMock创建一个虚拟的UserRepositoryInjectMocks创建真正的UserService并且Mockito会自动把Mock的Repository注入进去。这里要求UserService的属性是通过构造器注入的如果是字段注入InjectMocks可能注入失败这也是为什么我推荐Spring项目尽量使用构造器注入的原因之一。when(...).thenReturn(...)和verify(...)前者定义了当调用这个方法时返回什么后者验证这个方法确实被调用过几次。一正一反配合起来既能验证结果是否正确又能验证依赖协作是否符合预期。Service层的测试跑起来飞快十几个测试用例通常都是几百毫秒内搞定能做到随时跑、随时出结果这就是我们想要的反馈速度。3.2 Repository层测试DataJpaTest与真实数据库Service层的伪依赖测试完成之后Repository层有一个绕不过去的问题JPA/SQL的语法正确性Mock是验证不了的。findByEmailAndStatus这种由Spring Data JPA自动生成的方法名如果拼写错误在单元测试阶段根本不会暴露只有真正连上数据库跑一下才会报错。对这种场景SpringBoot提供了DataJpaTest注解。它的设计思路是只加载JPA相关的配置不加载Controller和Service这样既避免了启动整个上下文又保留了和真实数据库交互的能力。默认情况下DataJpaTest会使用内嵌数据库比如H2来测试如果项目用的是MySQL且SQL语句里有MySQL特有的语法也可以改成连一个专用的测试数据库。DataJpaTest class UserRepositoryTest { Autowired private TestEntityManager entityManager; Autowired private UserRepository userRepository; Test void shouldFindUserByEmailWhenUserExists() { // given User user new User(); user.setUsername(lisi); user.setEmail(lisiexample.com); entityManager.persistAndFlush(user); // when OptionalUser found userRepository.findByEmail(lisiexample.com); // then assertThat(found).isPresent(); assertThat(found.get().getUsername()).isEqualTo(lisi); } }这里面TestEntityManager是一个专门为测试设计的工具类它解决了测试方法之间数据隔离的问题。每一个测试方法执行完成后Spring会自动回滚事务所以你不需要自己写delete from user之类的清理SQL。还有一点DataJpaTest默认只会扫描Entity和Repository不会加载Service或Controller。这意味着如果你的实体类里有自定义的Version乐观锁字段测试插入数据的时候要注意版本号是否正确否则可能会出现乐观锁异常。这种事看起来小排查起来却挺花时间。3.3 Controller层测试WebMvcTest与MockMvcController层应该测什么我最常看到的情况是——大家把Controller当成了必须启动整个项目才能测的部分所以要么不测要么把整个SpringBoot应用SpringBootTest跑一遍再手动发起请求。这两种做法其实都有问题。正确姿势是用WebMvcTest配合MockMvc。WebMvcTest(TestController.class)只加载Web层的配置包括MockMvc、过滤器、拦截器和指定的Controller不会加载Service、Repository等Bean。Service依赖需要我们手动用MockBean来提供。WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockBean private UserService userService; Test void shouldReturnUserDetailWhenCallGetApi() throws Exception { // given UserVO userVO UserVO.builder() .id(1L) .username(zhangsan) .email(zhangsanexample.com) .build(); when(userService.getUserById(1L)).thenReturn(userVO); // when then mockMvc.perform(get(/api/users/1) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.id).value(1)) .andExpect(jsonPath($.username).value(zhangsan)) .andExpect(jsonPath($.email).value(zhangsanexample.com)); } Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.getUserById(999L)) .thenThrow(new UserNotFoundException(用户不存在)); mockMvc.perform(get(/api/users/999)) .andExpect(status().isNotFound()); } }写Controller层测试最需要注意的是MockMvc的返回结果验证。jsonPath($.username)的路径写法要和Controller返回的JSON结构保持一致取出嵌套字段时要注意层级关系比如data.user.username这种路径。还有状态码的断言404、400、500这些不同错误的语义要搞清楚不要只在成功路径上做断言异常路径才是Controller测试最容易发现Bug的地方。4. 切片测试与全量测试的边界别把所有东西都塞进SpringBootTest把所有测试都用SpringBootTest写在同一个上下文里是新手项目里最常见的问题。这样做的直接后果是整个测试类的启动时间越来越长而且不同测试类之间还会互相干扰比如一个测试类里的MockBean会污染Spring的全局缓存上下文导致其他测试类拿到错误的Bean。这一节细聊三种测试方法的边界。4.1 三种测试注解怎么选注解加载范围典型场景执行速度WebMvcTest只加载Web层Controller、过滤器、拦截器Controller的HTTP行为测试较快DataJpaTest只加载JPA/数据访问层Repository的SQL正确性测试较快配H2更佳SpringBootTest加载完整Spring上下文跨层联调测试、配置生效性验证慢通常数秒到数十秒我的核心建议是默认优先使用切片测试SpringBootTest只留给真正需要完整上下文的场景。比如你写了一个自定义的全局异常处理器它依赖RestControllerAdvice和若干个Filter这时候用WebMvcTest就够了但是如果你要验证配置文件里的某个ConditionalOnProperty是否注册了指定的Bean这时候只能靠SpringBootTest把整个上下文拉起来。SpringBootTest还有一个让人头疼的特性它会为每个配置类创建新的上下文缓存配置类多的时候测试时间可能是几分钟起步。之前维护过一个相对复杂的项目自动配置类有十几个跑一遍全量测试要八分钟左右CI十分钟的限制直接超时。后来把所有能用WebMvcTest和DataJpaTest的测试都切过去之后全量测试时间压到了两分钟以内效果立竿见影。4.2 Transactional的滥用问题在Repository测试里事务回滚是一件方便的事。但有些人会把Transactional加到Service层测试甚至Controller层测试上这其实是很危险的做法。Transactional意味着测试方法里的所有操作都在一个事务里执行如果被测代码内部自己开启了新事务比如REQUIRES_NEW传播级别或者在事务提交之后才生效的某些行为比如异步事件、消息队列这些在测试里是看不到真实效果的。换句话说加了Transactional的测试可能测的是一个虚假的现实——你以为业务逻辑都对实际上事务一提交就暴露问题。我见过一个真实的Bug代码里在Service方法内发送了Spring事件事件监听器里去做积分变更。单元测试里因为Transactional没提交事件在事务提交后才会真正触发有些配置是事务提交后发送导致测试事件监听器没有被执行测试却显示通过。这种问题是测试框架本身无法感知的所以我的建议是在Service层测试里不要加Transactional需要验证的数据库交互通过Mock对象来模拟。在Repository层测试里默认依赖DataJpaTest自带的事务回滚机制不需要手动加Transactional。如果需要验证Transactional(propagation Propagation.REQUIRES_NEW)这类特殊传播行为建议直接用真实的SpringBootTest跑集成测试不要用事务包裹的场景。4.3 测试隔离与数据污染给CI留条后路测试隔离是个老生常谈却又不断踩坑的话题。最常见的问题出现在Repository测试里两个测试方法都操作同一个表A方法插入了一条username common的数据B方法也准备插入同样一条结果unique约束冲突了。DataJpaTest默认的事务回滚能解决大部分问题但有一个例外需要注意如果你在测试方法里调用了entityManager.flush()之后又执行了原生SQL那部分操作可能超出了事务回滚的控制范围。再比如用native query或Query注解了INSERT/UPDATE语句Hibernate的一级缓存不一定能感知到容易产生脏数据。为了给CI留一条退路我的个人习惯是在每个测试类的最后加一个清理方法AfterEach void cleanUp() { userRepository.deleteAll(); }当然如果事务回滚机制正常工作这步是多余的。但万一哪天有人改了DataJpaTest的配置、或者引入了Flyway/Liquibase这类工具导致回滚逻辑变化这条保险能帮你省下大量的排查时间。5. 踩坑实录我在SpringBoot测试里栽过的跟头说完了方法论来点实战中经常卡住人的具体坑。这些坑我都实际踩过每一个都花了不少时间才找到原因。5.1 MockMvc返回中文乱码第一次用MockMvc跑Controller测试断言信息里包含中文结果一直报错。后来发现SpringBoot在测试环境下响应体的默认字符集可能是ISO-8859-1导致中文变成了乱码。解决办法是在MockMvc的请求上显式指定接受字符集mockMvc.perform(get(/api/users/1) .accept(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE)) ...或者更稳妥的做法在Controller上统一配置produces application/json;charsetUTF-8。如果用的是Spring Boot 2.x还可以在application-test.yml里配置server: servlet: encoding: charset: UTF-8 force: true这样测试环境下的响应编码就会是UTF-8。注意这个配置在Spring Boot 2.2之后默认已经强制为UTF-8所以如果你用的还是老版本这个坑很容易遇到。5.2 Mockito的when不生效方法签名/泛型的坑Mockito最让人摸不着头脑的问题就是我明明写了when(...)但测试里调用时还是返回了null。我自己遇到过几种原因按概率从高到低排第一种该方法的调用参数和when里给的参数不相等。比如测试时传入的是user.getId()而Mock时写的是固定值1L看起来结果应该一样但getUserId()返回值是空的实际传进去的是null匹配自然失败。这种问题用any()或者eq()就能解决when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));第二种被Mock的方法是final的或者Mock对象不是一个接口类型。Mockito默认是通过生成子类来实现Mock的如果方法被final修饰或者类本身被final修饰Mockito就没办法拦截。从Mockito 2.x开始针对final方法的Mock需要额外配置mockito-inline支持项目里如果没有显式引入遇到这个问题就会悄悄返回默认值。第三种方法返回值是void却用了when(...)。void方法应该用doThrow(...).when(...)或者doNothing().when(...)如果你用when去Mock一个void方法编译器会直接报错或者运行时报错。5.3 测试之间的数据污染同一份静态状态还有一个很隐蔽的坑发生在测试使用静态状态时。比如一个工具类里有个静态Map缓存或者一个单例Bean内部维护了一个List。如果第一个测试方法往里面塞了数据第二个测试方法在同一个JVM进程里运行读到的就是被污染的数据。解决这个问题的思路一是每个测试方法开始前进行初始化BeforeEach void setUp() { cache.clear(); }二是尽量避免在测试里依赖不可控的全局状态。如果必须依赖就用AfterEach在方法结束后清理干净。这种问题通常不会在本地测试时暴露——因为本地可能每个测试类单独跑但你一旦上了CI多个测试类串行或并行执行问题就出现了。所以保持测试方法之间的独立性是写好测试的基本功。5.4 MockBean的副作用与Spring上下文缓存污染MockBean非常方便但它有一个不那么明显的毒性它会替换Spring容器中的真实Bean而且这个替换会影响整个测试上下文缓存。如果测试类A用MockBean把UserService替换掉了随后运行的测试类B如果也想用真实的UserServiceSpring会复用之前那个缓存了Mock Bean的上下文导致B测试得到的也是Mock对象结果跟预期不符。这个问题没有特别干净的解法我能给出的建议是尽量把使用MockBean的测试类单独归拢在一起不要和需要使用真实Bean的测试类混在同一个运行批次里。使用DirtiesContext注解标注在MockBean用完后需要销毁上下文的测试类上强制Spring重新创建上下文。尽可能用WebMvcTest这类切片测试把MockBean的影响限制在Web层。5.5 使用JSONAssert比对复杂JSON结构有时候接口返回的JSON层级很深靠jsonPath逐字段断言太繁琐。JSONAssert可以把整个JSON字符串和预期字符串做结构化比对只关心key和值的类型不关心顺序非常适合验证一个复杂的返回结构。String expected {id: 1, name: zhangsan, address:{city:Beijing}}; JSONAssert.assertEquals(expected, responseBody, JSONCompareMode.LENIENT);这里JSONCompareMode.LENIENT表示期望里有的字段JSON里必须也要有但JSON里多出来的字段不算错STRICT则要求两边字段完全一致。写接口测试时可以根据场景灵活选择。6. 测试驱动开发在SpringBoot项目中的落地心得写测试的经验积累到一定程度之后自然就会开始考虑要不要先写测试再写代码的问题。关于TDD测试驱动开发我的观点是不要神话它但在合适的场景下它确实能显著提升代码质量。6.1 红绿重构的节奏我在写新的Service方法时尝试过先写测试再写实现的节奏。流程大概是这样的根据需求理清楚方法的输入、输出、异常路径先写一个会失败的测试红色。写最简单的代码让测试通过绿色。对代码进行重构抽象出必要的逻辑保证测试仍然通过。这套方法最大的价值不是先写后写的顺序而是逼着你在写代码之前先想清楚这个方法到底应该怎么表现才算正确。很多实际项目里开发人员拿过来需求就直接写实现写完再补测试或者不补等到联调的时候才发现接口行为不一致然后两个团队来回扯皮。先写测试等于把评审自己写的接口行为这件事提前到了开发阶段成本低了很多。但我也要强调TDD并不适合所有场景。写一个验证用户密码加密的工具类用TDD非常合适但在探索一个从未用过的第三方库API时你连它的返回结果长什么样都不确定这时候写TDD就是自找麻烦。这种场景我通常是先写一个临时的小Demo把第三方库的行为调通摸清再回到TDD的节奏里把正式逻辑写出来。6.2 测试命名规范让人一眼看懂在测什么好的测试代码读起来应该像一份需求文档。我比较推荐的命名方式是should_xxx_When_xxx用英文写下预期行为和触发条件Test void shouldThrowExceptionWhenUsernameAlreadyExists() { } Test void shouldCreateOrderWhenUserBalanceIsEnough() { }这个命名风格的好处是测试失败时看到方法名就知道是哪块业务出问题了不用点进源码一行行找。有些团队用中文方法名比如当用户名已存在时应该抛出异常也可以接受关键是团队内部保持一致。我在实际项目里还会给测试类做分组比如UserServiceTest拆成UserServiceCreateTest、UserServiceQueryTest、UserServiceDeleteTest这样每个测试类的职责更清晰跑测试时选中的范围也更小效率更高。6.3 覆盖率不是一切但没有覆盖率万万不行很多团队会把行覆盖率作为KPI指标比如覆盖率必须达到80%以上。我的看法是覆盖率是个参考指标但不是质量指标。一个坑人的测试可能覆盖率100%却没有任何有效断言而一些重要的复杂分支代码可能覆盖率不到50%一旦出Bug就是大事故。我比较推荐的做法是JaCoCo来统计覆盖率但把关注点放在关键路径和分支覆盖率上而不是总的行覆盖率。具体点说Service层里关键的if/else分支、异常处理分支这些一定要被测试覆盖到而Configuration类的getter/setter、Controller层的空逻辑方法覆盖不到也没太大影响。注意与其追求覆盖率数字不如保证每个核心方法都有至少一个正常路径用例和一个异常路径用例。这是我能给出最实用的建议。6.4 从零开始推动测试文化如果你所在的项目一直没有测试习惯想推单元测试千万不要一上来就要求所有代码全覆盖。我从实际经验里的建议是先挑一个最容易出Bug、修改频率最高的Service类把它的测试补全然后提交到代码仓库。之后做新功能时约定新写的逻辑必须附上测试老代码逐步补。这样既控制了成本又让团队逐步建立对测试的信任感——测试天天都在跑出了Bug就会改改完就有保障这套正向循环一旦建立起来往后推什么都容易。7. 最后分享一点个人体会做了这么多SpringBoot项目单元测试给我最大的感受不是多了一堆额外工作而是我终于可以放心大胆地改代码了。刚接手一个老项目的时候数据库结构可能已经和代码脱节配置文件里藏着一堆不知道干什么用的Bean哪个都不敢动。后来把和核心业务关联的几个Service和Repository的测试补齐之后再做数据库字段调整、配置项变更时跑一遍测试就能告诉我会不会影响现有逻辑这种掌控感比什么评论都实在。如果你想现在就开始实践我的建议是不要等到项目结构完全清晰了再写测试那样的时机永远不会到。找一个小模块把环境搭建好写几个基础用例跑通然后再一步步扩展。项目里的单元测试就像保险——你希望它永远不出险但万一出了事你会庆幸当初买了它。这套思路放到今天的SpringBoot项目开发里仍然是每一份高质量代码落地的基础设施。

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

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

免费获取报价