news 2026/9/13 7:55:37

Spring Boot单元测试保姆级教程:从Mockito到Testcontainers实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot单元测试保姆级教程:从Mockito到Testcontainers实战

把一个Spring Boot项目做到中后期,我最怕听到的一句话就是“这功能我本地能跑通啊”。说这句话的同事,十次里有八次没写单元测试。刚入行那阵,我也觉得测试是浪费时间——业务都写不完,哪来的功夫写测试?后来被线上事故打脸,又在一个几乎没有测试的老项目里挣扎了大半年,才彻底想明白一件事:单元测试不是给代码上保险,它是逼着你在写代码的时候就把问题想清楚——而这才是它最大的价值。

这篇保姆级教程写给所有想认真把Spring Boot单元测试做起来的人,不管你刚接触还是已经写了几个测试但总感觉不得要领,我尽量把从环境搭建到各层写法、再到排查调优的完整路径一次性讲透。我不会去堆叠那种复制就能跑的假demo,重点放在每个写法背后的“为什么”,以及那些不踩一遍根本不知道的坑。

1. 单元测试到底测什么:先想清楚这几层再动手

1.1 四层架构与每层的测试目标

Spring Boot最常见的组织方式是四层结构:Controller层负责接收请求和参数校验,Service层处理业务规则,Repository层操作数据库,最底层是数据库本身。单元测试的目标,是把这四层像切豆腐一样切开,逐块验证,不让跨层依赖干扰判断。

我在评审代码时经常看到一种误解,就是觉得“测试覆盖率到了80%就行”,于是把一堆逻辑全堆在Controller里,然后对着Controller写了大量测试。结果Service层的核心规则一点没测,Controller的测试又极度依赖请求和响应结构,改个字段名能碎掉十几个方法。这就是分层没想清楚导致的。

正确的做法是:Repository测试关注SQL和映射是否正确,Service测试关注业务规则和异常分支,Controller测试关注路由、参数校验、状态码和响应结构。层与层之间用Mock把边界切断,谁出问题一眼就能定位。这也是为什么我坚持在项目里把单元测试按层划分,而不是一股脑写进一个大测试类。

1.2 测试金字塔与Spring Boot项目的实际比例

测试金字塔是老生常谈,但在Spring Boot项目里落地时有一个务实比例,我自己的倾向是:Repository测试占20%左右,Service测试占60%左右,Controller测试占20%左右。Service是业务核心,分支最多,也最值得投入;Controller测试用MockMvc模拟HTTP请求,写起来轻快,但没必要覆盖所有参数组合;Repository测试只需要验证CRUD和自定义查询。

这样一个比例跑起来会非常舒服:Service测试不启动Spring容器,纯JUnit加Mockito,跑完几十个方法也就一两秒;Controller测试用切片方式只加载Web层,顶多几秒;Repository测试因为要处理真实SQL,稍微慢一点,但用H2内存库也能控制在可接受范围。

有个反例我记得很清楚:一个同事为了省事,用@SpringBootTest写了200多个测试用例,每次跑全量测试要5分钟。后来把大部分Service层测试改成纯Mockito方式,时间直接降到了40秒。这个体验上的差距会直接影响团队是否愿意坚持跑测试——测试跑得慢,人就下意识逃避,再好的测试文化也撑不住。

1.3 单元测试和集成测试的边界

这里我想多说一句边界问题。单元测试最大的忌讳是“把整条链路搬进来”。比如Service测试里又启动真实数据库、又连Redis、还要走消息队列,那就不叫单元测试了,叫集成测试。单元测试的定位是“快速反馈、精准定位”,一旦依赖了太多外部组件,测试就变得脆弱且缓慢。

那什么时候该写集成测试?我的习惯是:对外部API对接、数据库事务、缓存一致性、消息收发这四类场景,单独建一个integration包,用@SpringBootTest启动完整上下文,或者用Testcontainers拉起真实依赖来验证。集成测试不需要多,每条核心链路有一到两个用例兜底即可。剩下的细节交给单元测试去盯。

这样分工之后,全量测试的层次就清晰了:最快的单元测试在push前跑,集成测试在CI里跑,两者各司其职,谁也不拖谁的后腿。

2. 把测试环境铺平:依赖、目录与公共基类

2.1 Maven依赖:spring-boot-starter-test足够了

Spring Boot的测试依赖核心就是spring-boot-starter-test,一个依赖里打包了JUnit 5、Mockito、AssertJ、Hamcrest、JSONAssert、Spring Test等一大套东西,基本上日常需要的都在里面了。用Maven的话,加一段就行:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

如果你用的是Spring Boot 3.x以上,JUnit 5和Mockito的版本都已经帮你调好了,不用手动指定版本。Spring Boot 4.x我试过,整体API没有伤筋动骨的变化,但底层Jackson那部分可能有兼容调整,比如JsonMapper$Builder相关的问题——如果你升级到4.x之后测试里序列化突然报错,优先检查Jackson版本和ObjectMapper的构建方式,这属于版本适配的老问题。我自己主力用的还是3.x版本,团队项目升级4.x时会额外关注这类基础库的联动变化。

如果你的项目里需要操作JSON断言,spring-boot-starter-test里的jsonassertJsonPath也够用了。有一个小坑是:如果有旧项目依赖了老版本的hamcrest或者junit,可能会产生版本冲突,症状就是NoSuchMethodError或者Mockito的MockitoAnnotations无法正常工作。遇到这种情况,先看是不是junit-vintage-engine被带进来了,它是用来跑JUnit 4用例的,纯新项目没必要保留它。

2.2 测试目录与命名规范

目录结构上,Maven的约定是src/test/javasrc/test/resources,这个没什么好说的。真正需要注意的,是测试类的命名和分包方式。

我推荐按层分包,而不是按功能模块分包。什么意思呢?就是你做一个用户模块,不要建一个com.example.user包然后把测试全塞进去,而是建repositoryservicecontroller三个测试包,对应放UserRepositoryTestUserServiceTestUserControllerTest。这样有一个显而易见的好处:跑某一个层的测试时,你可以直接用IDE跑整个包,而不需要逐个挑选测试类。

命名规范上,我坚持用XxxTest去命名单元测试,用XxxIT去命名集成测试。Maven的maven-surefire-plugin默认只跑*Test.java,而maven-failsafe-plugin*IT.java。这意味着你可以在单元测试阶段跳过耗时的集成测试,CI里再单独拉一个job去跑它们。别不重视这个区分,项目规模大了之后,这能帮你省下大把CI时间。

2.3 打造一个公共测试基类:减少样板代码

测试里面最烦人的事情之一,是每个测试类都要写一堆重复的配置和Bean。我在实战里会抽两个基类出来,一个是纯单元测试基类,一个是集成测试基类。

单元测试基类可以很简单,只做Mockito的初始化:

@ExtendWith(MockitoExtension.class) public abstract class BaseUnitTest { // 公共的测试工具方法、常量可以放在这里 }

集成测试基类则负责加载Spring上下文、配置测试数据源、清理数据等:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public abstract class BaseIntegrationTest { @Autowired protected MockMvc mockMvc; }

这里我想解释一下@TestMethodOrder。默认情况下JUnit的方法执行顺序是不确定的,而很多数据库相关测试对数据状态有依赖,一旦顺序变化,测试就可能偶发失败。加了@TestMethodOrder(MethodOrderer.OrderAnnotation.class)之后,你就能通过@Order(1)@Order(2)显式控制顺序。当然,最理想的情况是每个测试方法都完全独立、互不依赖,但实际操作中总有一些场景(比如测试一个生成唯一编号的流水号逻辑)需要按顺序跑。我给的方案是:优先追求测试独立性,实在做不到再控制顺序。

2.4 application-test.yml:测试专用的配置

测试环境的配置和生产环境一定要分开,最直接的做法是在src/test/resources下放一个application-test.yml。里面我通常会配置这几个东西:

  • 数据库连接:本地测试用H2或Testcontainers,不用生产数据库
  • 日志级别:测试时把SQL日志打开,方便排查问题
  • 一些外部服务的mock开关:比如短信、支付、消息队列等,测试环境统一走mock实现
spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: create-drop show-sql: true cache: type: simple

这里的ddl-auto: create-drop意思是每次启动测试上下文时自动建表,结束后自动删除。这个配置在本地测试里非常方便,但要注意:create-drop只对H2这种内存库友好,如果你用Testcontainers养的MySQL容器,建议用none或者validate,然后让Flyway或Liquibase去管表结构,否则测试了一堆数据结构和线上对不上,就失去测试的意义了。

3. 各层测试怎么写:Repository、Service、Controller完整实操

3.1 Repository层测试:确认SQL和映射真的没问题

Repository层的测试要有真实数据库参与,哪怕是个内存库,这样才能验证JPA的命名规则、@Query里的JPQL、分页排序、乐观锁这些机制是否生效。我用的方案是@DataJpaTest,它只加载JPA相关的配置,不加载整个Spring上下文,跑起来很快。

一个典型的Repository测试长这样:

@DataJpaTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test void testFindByEmail() { User user = new User(); user.setEmail("test@example.com"); user.setName("张三"); userRepository.save(user); Optional<User> found = userRepository.findByEmail("test@example.com"); assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("张三"); } }

你可能会问,为什么不mock Repository?因为Repository本身就是一个数据访问层,如果你mock它,那测试就变成在验证自己写的mock逻辑,MySQL的SQL语法问题、字段映射问题全都被掩盖了。@DataJpaTest默认用的是内嵌数据库,所以只要依赖里有H2,就能直接跑起来。

这个测试里有个细节:findByEmail是JPA根据方法名自动生成的查询,如果你把实体里的字段名写错了,比如实体叫emailAddress,你方法名叫findByEmail,运行时直接报错。这种错误写代码时根本发现不了,但一个测试用例就能在几秒内暴露出来。所以Repository层的每个查询方法,我都会至少写一个正向用例。

3.2 Service层测试:业务规则和异常分支是重点

Service层是业务逻辑的大本营,也是我写测试投入最多的地方。Service的测试思路很简单:把Repository等其他组件全部mock掉,只验证Service自身对输入的处理和状态流转。

@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void testCreateUser_duplicateEmail_shouldThrow() { User user = new User(); user.setEmail("test@example.com"); when(userRepository.findByEmail("test@example.com")) .thenReturn(Optional.of(user)); assertThatThrownBy(() -> userService.createUser(user)) .isInstanceOf(BusinessException.class) .hasMessageContaining("邮箱已被注册"); } }

这个测试覆盖了一个关键业务规则:邮箱重复时,必须抛出业务异常阻止创建。when(...).thenReturn(...)是Mockito向被测方法“预定答案”的方式,assertThatThrownBy则来自AssertJ,专门做异常断言。

写Service测试时,我特别关注三个分支:正常路径、边界条件、异常路径。一个业务方法,如果正常路径返回结果,异常路径抛出固定的业务异常,边界条件有特殊值,那就至少写三个测试。比如一个计算折扣的方法,我会分别测试100元打八折得到80元、0元输入直接返回0、负价格输入抛异常。这些分支看起来简单,但它能防止你以后改代码时不小心把某个边界搞坏。

3.3 Controller层测试:MockMvc模拟完整HTTP请求

Controller层测试我用@WebMvcTest加MockMvc的组合,只加载Web层的东西,不启动整个Spring Boot上下文,速度很快。

@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testGetUserById() throws Exception { User user = new User(); user.setId(1L); user.setName("李四"); when(userService.getUserById(1L)).thenReturn(user); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("李四")); } }

这里有三个关键点要说清楚。第一,@MockBean会把Service替成一个mock,所以Controller测试只关心HTTP层的行为:路由对不对、参数校验是否生效、返回状态码和结构对不对。第二,jsonPath是JSON断言的利器,$.name表示JSON根节点下的name字段,这套表达式语法是JSONPath的标准,值得专门花半小时熟悉一下。第三,MockMvc的perform从语义上看起来是HTTP调用,但实际并没有发起网络请求,它在应用内模拟了整个请求处理链路,所以速度非常快。

我还习惯在Controller测试里覆盖参数校验场景。比如一个创建用户的接口要求email必填且格式合法,我会故意传一个空邮箱,然后断言返回400。这样整个参数的约束逻辑就被测住了,以后改参数上的@NotNull注解时,测试会第一时间告诉你有没有漏掉什么。

3.4 上传文件和WebSocket的测试补充

项目里如果Controller有文件上传功能,测试很容易被忽略,但这也是可以测的。Spring官方提供了MockMultipartFile,配合MockMvc可以模拟multipart请求:

MockMultipartFile file = new MockMultipartFile( "file", "test.txt", "text/plain", "hello world".getBytes()); mockMvc.perform(multipart("/api/upload") .file(file) .param("description", "测试文件")) .andExpect(status().isOk());

WebSocket的测试就麻烦一些,@WebMvcTest覆盖不了,需要用真正的WebSocket客户端连上来测。我的做法是把WebSocket的广播逻辑抽到Service层,对Service做单元测试,然后写一个集成测试类,用@SpringBootTest启动实际端口,再用WebSocketClient连接验证广播和群组消息。

4. Mockito和断言库用熟了,测试代码能少一半

4.1 Mockito核心API速查与使用场景

Mockito是Spring Boot测试里绕不开的重型武器。它的核心思想是:把依赖的对象替身,然后用when定义替身行为,用verify验证替身是否被正确调用。下面这几个API是我日常使用频率最高的:

场景推荐写法说明
定义返回值when(mock.method()).thenReturn(value)最常用的打桩方式
定义空返回值方法doNothing().when(mock).delete(id)用于void方法
抛出异常doThrow(new RuntimeException()).when(mock).save(user)测试异常分支
验证调用次数verify(mock, times(1)).save(user)验证方法是否被调用
捕获参数ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class)验证传递的参数
部分mockMockito.spy(realObject)只mock其中几个方法

ArgumentCaptor是个很好用但容易被忽略的工具。比如一个创建订单的方法,内部会把多个商品添加进订单里,你光看订单总数是对的,但想确认商品列表里确实包含某个商品时,就需要通过ArgumentCaptor把传给Repository的实体捕获出来,再做里面的字段断言。

4.2 AssertJ链式断言:让断言本身更可读

老项目里经常能看到JUnit原生的assertEquals(1, order.getTotalCount())这种断言,写法也没有错,但可读性和报错信息都差一些。我更推荐AssertJ的链式API:

assertThat(order) .hasFieldOrPropertyWithValue("status", OrderStatus.CREATED) .extracting(Order::getItems) .asList() .hasSize(2);

同样是断言,AssertJ的风格是“先拿到对象,再一步步描述它应该是什么样”,写出来的测试几乎就是自然语言。而且AssertJ对集合有非常丰富的断言方法:containsExactlyextractingfilteredOn,配合Stream风格的API,能很优雅地处理对象集合。

4.3 断言JSON响应:JSONAssert和jsonPath怎么选

Controller测试里要做JSON响应断言时,有两个选择:一是用jsonPath对单个字段做精确定位,二是用JSONAssert做整体比对。我自己的选择是:关心结构时用jsonPath,关心整体快照时用JSONAssert

JSONAssert可以做“非严格模式”比对,意思是不管字段的排列顺序,只关注JSON结构是否一致:

String expected = "{\"name\":\"李四\",\"age\":20}"; String actual = mockMvc.perform(get("/api/users/1")) .andReturn() .getResponse() .getContentAsString(); JSONAssert.assertEquals(expected, actual, JSONCompareMode.NON_EXTENSIBLE);

这种写法的好处是:当你只是想确认返回结果和某个期望JSON一致时,比一行行jsonPath去断言字段要省事很多。但弊端是它比较脆弱,接口多返回一个字段就容易失败,所以一般我只在关键链路的测试里用。

5. 让测试稳、快、不互相污染:事务、隔离与Testcontainers

5.1 @Transactional测试回滚的机制与坑

集成测试里如果测试方法操作了数据库,会带来一个头疼的问题:数据残留。第一次测试插了一条email=test@example.com的用户,第二次再跑同一个用例时,唯一约束直接报错。Spring对这个问题有一个标准解法,就是在测试类或测试方法上加@Transactional

加了@Transactional之后,Spring会把整个测试方法包在一个事务里,测试跑完自动回滚,数据库回到测试之前的状态。这是我在@SpringBootTest@DataJpaTest的测试里默认都会加的一个注解。

但这里有个非常隐蔽的坑:如果你在测试里用@Async异步调用某个Service方法,异步线程执行时拿不到测试方法的事务,数据不会被回滚,反而可能卡在数据库里。还有一个更常见的场景:你测试的代码里调用了TransactionTemplate来开启新的事务,这个新事务不会跟随测试方法回滚。遇到这种情况,我会手动清理测试数据,在@AfterEach里执行DELETE语句,或者给每一条测试数据生成一个随机后缀,让它天然不冲突。

5.2 测试执行顺序:从无序中找到可控性

JUnit默认的测试方法执行顺序是确定的,但不是按照你写的代码顺序,而是有一个内部的确定性算法。这导致了一个经典问题:如果你的测试方法之间真的有数据依赖,那么有时跑三个方法全过,有时第三个方法失败,因为前两个方法的执行顺序变掉了。

解决这个问题的核心思路,是让测试方法之间不共享任何可变状态。每个测试方法自己创建数据、自己断言、自己清理。如果某些场景实在绕不开顺序(比如测试一个状态机的状态流转),就用@TestMethodOrder(MethodOrderer.OrderAnnotation.class)配合@Order注解显式指定顺序。注意这只能保证同一个测试类内部的方法顺序,跨测试类的顺序依然不受控制。

我个人的建议是:碰到必须依赖顺序才能通过的测试,先停下来想一想是不是设计上出了问题。很多时候,是因为业务方法里直接把“状态”作为参数贯穿了多个步骤,而不是用对象自身维护状态,导致测试很难构造独立的场景。

5.3 从H2走向Testcontainers:别让测试和生产环境差太多

H2内存库跑起来确实快,配置也简单,但它有个致命问题:H2是模拟的SQL语法,并不完全等价于MySQL或PostgreSQL。我在一个老项目里就吃过亏,GROUP BYORDER BY在H2里的行为跟MySQL不一致,H2测试全绿,上生产却直接报错。

现在我的做法是,图形化界面的后端项目一律用Testcontainers。它可以在测试时用Docker拉一个真实数据库容器,测试跑完自动销毁。配置也不算复杂:

@Testcontainers @SpringBootTest class UserIntegrationTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @DynamicPropertySource static void datasourceConfig(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } }

@DynamicPropertySource是Spring Boot 2.5之后引入的,用来动态覆盖配置属性,到了3.x已经非常稳定。Testcontainers最大的优势是“它就是真实的数据库”,所以SQL语法、索引行为、锁机制这些都能被真实覆盖到。代价是需要Docker环境,CI里也要装Docker。

如果你的机器不支持Docker,或者只想跑小规模的Repository测试,用H2兜底也可以。但请务必在测试配置里加上MODE=MySQL或对应的兼容模式,尽量减少和生产环境的差异。需要注意,这只能缓解问题,不能彻底消除差异。

5.4 Actuator端点的测试:安全配置别被漏掉

提到spring boot actuator未授权访问这个热词,我想多说一句:Actuator的端点测试经常被遗漏,但这恰恰是生产安全保障里很重要的一环。如果你引入了spring-boot-starter-actuator,建议至少写一个测试用例,确认敏感端点在没有认证的情况下不会被访问。

@SpringBootTest @AutoConfigureMockMvc class ActuatorSecurityTest { @Autowired private MockMvc mockMvc; @Test void testHealthEndpoint_accessibleWithoutAuth() throws Exception { mockMvc.perform(get("/actuator/health")) .andExpect(status().isOk()); } @Test void testEnvEndpoint_deniedWithoutAuth() throws Exception { mockMvc.perform(get("/actuator/env")) .andExpect(status().isUnauthorized()); } }

第一个测试确认健康检查端点可以公开访问,第二个测试确认环境变量端点被拦截。这类安全相关的测试,一旦加了认证逻辑,它能自动验证你的配置没有漏掉某个端点。

6. 常见问题排查与效率提速

6.1 常见问题速查表

写Spring Boot测试的过程中,有几个问题几乎每个项目都会遇到。我把它们整理成一个速查表,方便你直接定位。

症状可能原因解决方案
测试报NoSuchBeanDefinitionException用了@SpringBootTest,但Bean的加载条件不满足检查配置类扫描路径,或者改成切片测试只加载需要的部分
Mock不生效同一个Bean被@MockBean@Autowired同时引用统一使用@MockBean,或通过@SpyBean处理部分mock
测试数据互相污染测试方法之间没有隔离@Transactional回滚,或者在@AfterEach清理数据
@Value属性值为null测试上下文没有加载对应的配置文件@TestPropertySource@ActiveProfiles("test")
测试方法执行顺序导致的偶发失败方法之间隐式依赖使用@TestMethodOrder,但最理想是去除依赖
MockMvc请求返回404@WebMvcTest只加载了指定Controller确认@WebMvcTest(指定Controller.class)参数是否正确
H2报语法错误H2和生产数据库SQL差异太大换Testcontainers或设置H2兼容模式
测试特别慢每个测试类都加载完整Spring上下文@DataJpaTest@WebMvcTest切片替代
JSON断言一直失败日期格式或内部字段顺序不一致统一配置ObjectMapper,使用jsonPath做字段断言

这些问题的坑,我几乎一个个都踩过。最让我印象深刻的是一次NoSuchBeanDefinitionException,排查了半天才发现是测试类所在包路径没有被Spring扫描到——包路径比主启动类多了一层。后来我把@SpringBootTestclasses属性显式指定为Application.class,这个问题再也没有出现过。

6.2 测试跑得太慢:切片测试与并发策略

测试慢在大型项目里是个严重影响团队信心的事。我有两条提速路线:一条是正确使用切片测试,另一条是让测试并发执行。

切片测试的核心,是让Spring只加载测试内容真正需要的部分。Web层测试用@WebMvcTest,数据访问层测试用@DataJpaTest,JSON处理测试用@JsonTest,而不是一上来就@SpringBootTest。这些切片测试只加载局部的Bean,上下文启动速度快很多。

Spring Boot 3.2之后,JUnit 5原生支持测试类级别的并行执行。你可以通过junit-platform.properties配置:

junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent

并行测试可以显著提速,但我建议先确保每个测试都没有共享外部状态再开启,否则并行反而会造成大量偶发失败。我的经验是:把单元测试并行跑,集成测试串行跑,安全性更好。

6.3 代码覆盖率:看哪些指标才不算自欺欺人

覆盖率这个指标经常被误解。团队KPI要求行覆盖率80%,于是大家写了大量没有断言的测试——只要代码执行到了就算数,这其实是在自欺欺人。我见过一个项目覆盖率高达90%,但核心的Service层几乎没测,测的全是模板代码,这远远谈不上安全。

我更倾向于关注两个指标:分支覆盖率和变更覆盖率。分支覆盖率反映的是if-else、三目运算符、switch这些逻辑是否都被执行过;变更覆盖率反映的是本次提交新增或修改的代码,是否被测试覆盖到。这两个指标对发现测试盲区更有效。

在工具层面,本地开发我用JaCoCo生成报告,CI里再把它和SonarQube接起来。JaCoCo插件配置到Maven里就行:

<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.12</version> <configuration> <excludes> <exclude>**/entity/**</exclude> <exclude>**/dto/**</exclude> </excludes> </configuration> </plugin>

把entity和dto这些纯数据结构排除掉,覆盖率统计会更有指导意义。

6.4 从今天开始怎么引入单元测试

如果你的项目还是“零测试”状态,别想着一次性给所有模块配上测试,那既不现实,也会让人觉得“搞测试很痛苦”。我给的建议是三步走。

第一步,从Service层开始。选一个业务逻辑最集中、最需要保护的模块下手,把这个模块的Service测试补齐。Service测试不需要启动Spring上下文,写起来快,价值又最明显。

第二步,给Controller层写关键接口的MockMvc测试。不用每个接口都覆盖,优先选取涉及对外协议、权限校验和核心流程的接口。

第三步,再回头给Repository层补测试。特别是那些手写了JPQL、复杂查询、分页排序的方法,每个方法至少一个正向用例。

这三步走完,一个模块就有了基础的防护网。之后每次改这个模块的代码,你都敢直接跑测试看有没有改坏。等到你哪天改完一个几十行的业务方法、顺手跑完全部测试、3秒全绿的时候,你会回来感谢当初坚持写测试的自己。

最后再分享一个小技巧:写好测试的第一步,是写出能测的代码。当你在Service里一个方法塞了几十行逻辑、还new了各种依赖对象的时候,你没法测;当你把依赖都用构造方法注入、业务规则拆成小方法的时候,测试自然顺手。所以,别只把单元测试当成一道工序,它更像是检验你代码设计的一面镜子。每次写测试觉得特别费劲,我会停下来看代码,多半是设计出问题了——这个习惯,帮我少走了很多弯路。

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

AI Agent工程化落地:协议层、编排层与CLI实战指南

1. 为什么2026年AI Agent不是“下一个大模型”&#xff0c;而是工程化落地的分水岭&#xff1f;你刷到过多少条标题为《AI Agent将彻底取代程序员》《Agent时代已来&#xff0c;再不学就晚了》的短视频&#xff1f;我去年在三个技术社区做过抽样统计&#xff1a;73%的“Agent入…

作者头像 李华
网站建设 2026/9/13 7:52:48

后端技术栈避坑指南:这7个坑90%的人踩过

后端开发就像在雷区行走&#xff0c;技术栈越丰富&#xff0c;踩坑的概率越大。有些坑是教科书上的经典&#xff0c;有些则是只有经历过线上事故才会懂的痛。下面这7个坑&#xff0c;90%的后端都踩过&#xff0c;区别只在于踩得早还是踩得晚。坑一&#xff1a;盲目上微服务&…

作者头像 李华
网站建设 2026/9/13 7:52:24

半导体AI智能体:研发效率革命与落地挑战

1. 半导体研发AI智能体的行业背景与挑战半导体行业正面临摩尔定律放缓与技术复杂度飙升的双重压力。根据国际半导体技术发展路线图(ITRS)的数据&#xff0c;28nm制程研发成本约5000万美元&#xff0c;而7nm制程直接飙升至3亿美元。在这个背景下&#xff0c;AI智能体正在改变传统…

作者头像 李华
网站建设 2026/9/13 7:51:41

408数据结构入门:时间复杂度和空间复杂度全解析

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

作者头像 李华