把一个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里的jsonassert和JsonPath也够用了。有一个小坑是:如果有旧项目依赖了老版本的hamcrest或者junit,可能会产生版本冲突,症状就是NoSuchMethodError或者Mockito的MockitoAnnotations无法正常工作。遇到这种情况,先看是不是junit-vintage-engine被带进来了,它是用来跑JUnit 4用例的,纯新项目没必要保留它。
2.2 测试目录与命名规范
目录结构上,Maven的约定是src/test/java和src/test/resources,这个没什么好说的。真正需要注意的,是测试类的命名和分包方式。
我推荐按层分包,而不是按功能模块分包。什么意思呢?就是你做一个用户模块,不要建一个com.example.user包然后把测试全塞进去,而是建repository、service、controller三个测试包,对应放UserRepositoryTest、UserServiceTest、UserControllerTest。这样有一个显而易见的好处:跑某一个层的测试时,你可以直接用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) | 验证传递的参数 |
| 部分mock | Mockito.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对集合有非常丰富的断言方法:containsExactly、extracting、filteredOn,配合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 BY和ORDER 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扫描到——包路径比主启动类多了一层。后来我把@SpringBootTest的classes属性显式指定为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了各种依赖对象的时候,你没法测;当你把依赖都用构造方法注入、业务规则拆成小方法的时候,测试自然顺手。所以,别只把单元测试当成一道工序,它更像是检验你代码设计的一面镜子。每次写测试觉得特别费劲,我会停下来看代码,多半是设计出问题了——这个习惯,帮我少走了很多弯路。