ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Maven多模块工程聚合测试模块搭建:统一集成测试入口的工程实践

Maven多模块工程聚合测试模块搭建:统一集成测试入口的工程实践 Maven 多模块工程建多了之后有个问题迟早会找上门单测代码散落在各个业务模块里想统一跑一次全量测试得依次进每个模块执行mvn test排查“到底哪个模块挂了”全靠肉眼扫日志。尤其是 Spring Boot 项目模块之间还有依赖关系测试环境初始化慢、重复配置多CI 里一旦某个子模块漏测发布前根本发现不了。我在实际项目里踩过几次坑之后决定改变策略单独抽出一个“聚合测试模块”把所有跟业务无关的测试基建、公共断言工具、跨模块集成测试集中管理。这篇文章就完整梳理一下这套方案的搭建思路、关键配置和实战中会遇到的坑供团队内部和社区的朋友参考。1. 整体思路拆解为什么要为“测试”单独建一个模块1.1 先看常规做法的痛点在哪里先描述一下大多数人最开始的做法。直接在父 POM 里配modules里面放common、service-a、service-b、web等业务模块每个模块的src/test/java下各自写自己的单元测试。表面上看各模块自治但实际协作中问题很明显第一测试执行入口分散。本地开发时你只需要跑自己负责模块的测试这没问题但 CI 流水线做全量回归时你得在 Jenkins 脚本里写一串mvn -pl service-a test mvn -pl service-b test ...。如果中间某个模块构建失败后面的模块直接就断了你还要猜是环境问题还是代码问题。第二公共测试工具类重复建设。比如 JSON 比较器、数据库断言器、MockMvc 请求封装、随机测试数据生成器这些工具类在service-a和service-b里各写了一份仅仅是因为它们不在同一个模块。等你把代码一提炼发现至少能删掉 40% 的重复代码。第三跨模块集成测试无处安放。一个典型的业务场景是web模块接收 HTTP 请求调用service-a的接口service-a又依赖common里的某个组件。这种需要拉起完整 Spring 上下文的集成测试放在web模块里是不合适的因为它把service-a、common的测试职责也卷进来了。放service-a里也不合适因为web模块的 Controller 层它也依赖不到——Maven 模块依赖是单向的一个模块只能依赖它声明过的依赖。1.2 聚合测试模块的核心职责划分聚合测试模块的设计思路简单说就是六个字依赖倒置、统一收口。它不是要替代各个业务模块自己的单元测试——那些“针对单个类的方法级测试”依然保留在各自模块里因为它们跟业务代码的耦合度高代码内聚性也更强。聚合测试模块主要承接三类内容全模块的测试基建统一的 JUnit 扩展、Spring Boot 测试注解封装、测试配置类、公共 Mock 工厂、测试数据清理器等跨模块的集成测试需要同时拉起多个模块的 Spring 上下文验证模块间交互逻辑的端到端测试测试执行入口通过 Maven 依赖把所有业务模块的测试类路径引入进来执行全量测试统一生成测试报告和覆盖率。1.3 为什么不用 Maven 自带的 Aggregator有人可能会问Maven 本身就支持聚合直接在父 POM 里用mvn test就能跑所有子模块的测试何必再抽一个模块出来这个问题的答案在于聚合和继承的本质区别。父 POM 的mvn test是逐个 reactor 模块串行执行测试模块之间的测试类无法共享上下文你也无法在一个统一的地方配置跨模块的测试行为。更关键的是如果你想把“测试工具依赖”比如 Testcontainers、嵌入式数据库、内存 Redis和“业务运行依赖”彻底隔离父 POM 的依赖管理做不了这件事——它只是统一管理版本但每个业务模块 build 时依然会把这些依赖打进去。独立测试模块则不同。它只存在于测试生命周期里业务模块完全感知不到它。它在 Maven reactor 里天然排最后依赖所有业务模块但反过来业务模块不依赖它。这样一来你可以在测试模块里加任何重型的测试依赖而不会污染业务模块的 classpath。2. 工程结构调整父子模块与依赖关系设计2.1 推荐的项目结构一览先说结论我最终落地的结构长这样parent-pom ├── pom.xml // 父 POMmodules 统一声明 ├── common // 基础工具、公共实体 ├── service-a // 业务模块 A ├── service-b // 业务模块 B ├── web // Web 入口模块 │ └── pom.xml └── it-tests // 聚合测试模块单独拎出来 ├── pom.xml └── src ├── main/java // 测试模块也有 main放测试基建类 │ └── com/example/it │ ├── config/ │ ├── extension/ │ └── factory/ └── test/java // 整合测试、端到端测试 └── com/example/it └── integration/这里有个细节容易忽略聚合测试模块的测试基建类要放在src/main/java而不是src/test/java。原因是这些基建类本身会被测试代码依赖如果你放在src/test/java里就只能在当前模块的测试里使用无法被模块内的其他测试目录引用——虽然从物理路径上看它们是同一个模块。更直白的说聚合测试模块本身不对外提供业务能力它的 main 区域就是给 test 区域提供“测试工具库”的。2.2 父 POM 的 modules 配法父 POM 里的 modules 我用的是显式声明而不是把所有子目录自动扫描进来的方式。虽然 Maven 默认会搜所有包含pom.xml的子目录但显式声明的顺序其实有讲究——它影响着 reactor 的构建顺序modules modulecommon/module moduleservice-a/module moduleservice-b/module moduleweb/module moduleit-tests/module /modulesit-tests必须排在最后这在 Maven 里不是约定而是硬性约束因为它依赖了前面所有模块所以 reactor 构建时它天然就是最后一个。如果你把它放中间Maven 也能正确计算依赖顺序不会报错但日志里你就会看到 warning而且人类阅读这个 POM 文件时容易产生误解以为它和common是同层级的。2.3 聚合测试模块的 POM 配置详解it-tests模块自己的 POM 是整个方案的核心配置上要把握三个原则继承父 POM 的依赖管理但不继承父 POM 的所有依赖、显式引入需要测试的业务模块、测试专用依赖控制在最小范围。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIdparent-pom/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent artifactIdit-tests/artifactId packagingjar/packaging nameIntegration Test Aggregator/name dependencies !-- 业务模块依赖 -- dependency groupIdcom.example/groupId artifactIdweb/artifactId version${project.version}/version scopetest/scope /dependency dependency groupIdcom.example/groupId artifactIdservice-a/artifactId version${project.version}/version scopetest/scope /dependency !-- 测试框架依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency !-- 数据库测试支持内存 H2 -- dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scopetest/scope /dependency !-- 嵌入式 Redis 测试支持仅测试使用 -- dependency groupIdit.ozimov/groupId artifactIdembedded-redis/artifactId version0.7.3/version scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration includes include**/*Test.java/include include**/*IT.java/include /includes /configuration /plugin /plugins /build /project注意几点scopetest/scope一定要写清楚。业务模块依赖在测试模块里全部限定为 test 作用域这是一道安全防线万一it-tests被打包进某个发布产物虽然正常情况下不会业务代码也不会被带进去。同时它也向团队成员传递一个信号——这个模块就是测试用的。spring-boot-starter-test 里的 JUnit 5、AssertJ、MockMvc 全套都能在这直接用。因为父 POM 的 dependencyManagement 已经锁定了 Spring Boot 版本这里的测试依赖就不需要写 version 了避免版本混乱。插件配置上surefire 我显式把*IT.java也纳入了。这是故意为之在经典 Maven 约定里*IT.java是给 failsafe 插件跑集成测试用的surefire 默认只跑*Test.java。但对于一个纯测试模块你不希望把“单元测试”和“集成测试”用两套不同的插件执行机制分开因为它们的生命周期在 CI 里不好统一。全部归 surefire 跑命令只有一条mvn test省事。3. 核心配置与测试基建实现3.1 版本控制与依赖收口父 POM 的 dependencyManagement这套方案要跑得顺父 POM 里的 dependencyManagement 必须把跟测试相关的依赖版本都锁住。常见的做法是直接用spring-boot-dependencies作为 BOM 导入这样所有 Spring 相关依赖的版本都统一了dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement如果你在业务模块里各自声明测试依赖的版本那么时间一长绝对会出现service-a里 H2 是 1.4.200service-b里 H2 是 2.1.214然后it-tests里你引入的 H2 版本跟两者都不同。跨模块的集成测试最怕这种不一致——你在测试模块里初始化数据库的行为跟业务模块里跑单测的行为不一样排查时非常折磨人。统一 BOM 之后版本冲突这个问题直接在依赖管理层面就消失了。3.2 测试基类设计让所有集成测试复用同一个 Spring 上下文聚合测试模块里我设计了一个BaseIntegrationTest基类它是所有跨模块测试的父类。这个类解决一个核心痛点多模块工程里拉起 Spring 上下文时component-scan 的根路径怎么确定。先看实现package com.example.it.config; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; SpringBootTest(webEnvironment SpringBootTest.WebEnvironment.RANDOM_PORT) TestInstance(TestInstance.Lifecycle.PER_CLASS) ActiveProfiles(it) TestPropertySource(properties { spring.datasource.urljdbc:h2:mem:itdb;DB_CLOSE_DELAY-1;MODEMySQL, spring.jpa.hibernate.ddl-autocreate-drop }) public abstract class BaseIntegrationTest { Autowired protected TestRestTemplate restTemplate; Autowired protected ApplicationContext applicationContext; }关键点逐个说。SpringBootTest你要指定启动类。因为it-tests模块本身没有SpringBootApplication主类如果只写SpringBootTest不带参数Spring Boot 会在当前模块的 classpath 下找SpringBootConfiguration找不到就直接报错。这里我用了webEnvironment RANDOM_PORT避免测试端口冲突同时也让TestRestTemplate可以拿到真实端口发起 HTTP 请求。配置文件别放在业务模块里。it-tests模块自己维护一份src/test/resources/application-it.yml里面定义测试专用的数据源、Redis 地址、日志级别等。这样做有一个附带的好处业务模块泄露出去的application.yml里哪怕有生产环境的敏感配置也不会影响测试执行因为itprofile 全覆盖了。3.3 测试数据工厂与断言工具消灭重复代码的利器聚合测试模块的另一个重要价值在于公共测试代码的统一管理。我在it-tests的main目录下建了几个工具类这里讲两个最有价值的。一个是TestDataFactory专门负责构造跨模块交互时需要的复合业务对象。例如在电商场景里要构造一个“已支付订单”需要同时操作用户模块、订单模块、支付模块每个模块的领域对象构造函数都不一样。没有聚合测试模块的时候每个集成测试里都要写一段 20 行的 “准备数据” 代码有了工厂类之后就是一行调用TestDataFactory.OrderContext context TestDataFactory.createPaidOrder(user-token-001);另一个是RedisMockManager。因为部分业务模块在启动时会扫描 Redis 连接配置而 CI 环境里不一定有独立的 Redis 实例所以这个工具负责统一启动/关闭嵌入式 Redis并自动注入连接参数。用 JUnit 5 的BeforeAllCallback扩展实现package com.example.it.extension; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import redis.embedded.RedisServer; public class RedisExtension implements BeforeAllCallback { private static RedisServer redisServer; Override public void beforeAll(ExtensionContext context) throws Exception { if (redisServer null) { redisServer new RedisServer(16379); redisServer.start(); System.setProperty(spring.data.redis.host, 127.0.0.1); System.setProperty(spring.data.redis.port, 16379); } } }使用的时候在测试类上标注ExtendWith(RedisExtension.class)即可。类的静态性保证了整个测试期间只有一个 Redis 实例不会每个测试类都重新拉起一个端口不然测试并行跑起来会端口冲突。4. 实操过程从零到一搭建聚合测试模块4.1 步骤一创建独立测试模块工程我在 IDE 里建议直接用 Maven 命令创建骨架而不是手工建目录。进入父工程根目录执行mvn archetype:generate -DgroupIdcom.example \ -DartifactIdit-tests \ -Dversion1.0.0-SNAPSHOT \ -Dpackagecom.example.it \ -DarchetypeArtifactIdmaven-archetype-quickstart生成完以后手动调整三处删掉模板生成的App.java和AppTest.java补上src/main/java下的包结构把 POM 的parent指到父 POM。然后回到父 POM 的modules里追加moduleit-tests/module。这个创建过程有个小技巧不要用 IDE 的 New Module 向导自动导入因为它默认会帮你把 Maven 的packaging和依赖结构改了反而多出很多噪音。手动建 手动改 POM对 Maven 的理解会更深也方便后续做团队内部的模板。4.2 步骤二在父 POM 中开启 test-jar 支持这里有一个非常关键的配置细节不配的话业务模块的测试工具类无法共享给集成测试模块使用。默认情况下Maven 构建业务模块时只打包main下的代码src/test/java里的内容不会进入产出 jar。但我们经常遇到这样的情况service-a模块里已经有一个写得很好的MockOrderRepository集成测试里也想用可它放在service-a的src/test/java里it-tests模块依赖根本引用不到。解法是让你业务模块的 Maven 在 package 阶段同时生成一份 “test-jar”。在业务模块的 POM 里加build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId executions execution goals goaltest-jar/goal /goals /execution /executions /plugin /plugins /build然后在it-tests模块依赖时用type指定 test-jardependency groupIdcom.example/groupId artifactIdservice-a/artifactId version${project.version}/version typetest-jar/type scopetest/scope /dependency这是给“业务模块自身有测试工具但想复用”的情况准备的。实践里我建议不要一开始就大量用 test-jar因为测试工具代码跟业务模块的耦合度高复用到最后又变成一个“隐式公共模块”。优先是把公共性强的工具直接挪到it-tests的 main 区域test-jar 只作为备选方案。4.3 步骤三确保测试能够“感知”所有业务模块的类有一个容易被忽略的 bug集成测试类里引用了service-a中某个Service类的 FQN代码编译没问题但跑测试时 Spring 容器扫描不到因为SpringBootTest默认扫描的是当前测试类所在包及其子包。解决方式有两种我都用过各有利弊。第一种是显式指定扫描根路径SpringBootTest(classes {ServiceAApplication.class, ServiceBApplication.class, WebApplication.class})这种方式的优点是指定精准但缺点也很明显每次新增一个业务模块都要回过来改测试基类维护成本高。第二种是改SpringBootTest的classes属性加上一个通用的测试配置类在配置类里用ComponentScan指定多个 basePackageSpringBootConfiguration ComponentScan(basePackages { com.example.common, com.example.servicea, com.example.serviceb, com.example.web }) public class IntegrationTestConfiguration { }这样测试基类就变成SpringBootTest(classes IntegrationTestConfiguration.class)推荐用第二种。虽然 basePackages 多写了几个字符串但它不会因为业务模块的新增而破坏整个上下文加载最多是加一个新包名而已。实测下来这种显式配置比自动扫描更可控因为模块多了以后 Spring Boot 的组件扫描如果撞上了同名的Configuration类启动过程会出现一堆 “BeanDefinitionOverrideException” 或 “ConflictingBeanDefinitionException”。注意如果你在多个业务模块里各有一个Configuration类不巧用了相同的类名Spring Boot 会因为结构化冲突无法启动。聚合测试模块加载全量上下文时最容易暴露这类问题平时只跑单个模块的测试根本发现不了。4.4 步骤四编写第一个跨模块集成测试下面是一个真实的跨模块测试样例模拟的是通过 web 模块暴露的 HTTP API 创建订单验证 service-a 里的领域逻辑正确执行同时确认 common 模块里的缓存工具被正确调用package com.example.it.integration; import com.example.it.config.BaseIntegrationTest; import com.example.it.extension.RedisExtension; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; ExtendWith(RedisExtension.class) class OrderCreationIT extends BaseIntegrationTest { Autowired private TestRestTemplate restTemplate; Test DisplayName(通过 Web API 创建订单整个链路应该成功) void shouldCreateOrderViaFullHttpStack() { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); MapString, Object requestBody Map.of( userId, 1001L, productId, 2002L, quantity, 3 ); HttpEntityMapString, Object request new HttpEntity(requestBody, headers); ResponseEntityString response restTemplate.postForEntity( /api/orders, request, String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()) .isNotNull() .contains(\orderNo\); } }跑这个测试的命令跟跑单元测试一模一样cd it-tests mvn test如果你在父工程根目录下想只跑聚合测试模块也不用先构建所有模块——Maven reactor 会自动帮你构建依赖链除非你用了-pl it-tests而没加-am。正确写法是mvn -pl it-tests -am test-am全称--also-make意思是同时构建这个模块依赖的所有上游模块。不加-am你本地仓库里没有最新版本的service-a、web直接跑it-tests会报依赖缺失。5. 执行过程中的经典坑与排查手册5.1 坑一测试类被 surefire 忽略明明写了测试却显示 0这个场景我在新同事的电脑上碰到过折腾了一个下午。表现是mvn test控制台输出 “Tests run: 0”但 IDE 里跑测试明明能跑起来。原因通常是 surefire 插件默认的includes是**/*Test.java、**/Test*.java、**/*Tests.java、**/*TestCase.java。你把跨模块测试类命名为OrderCreationIT——后缀是IT而不是Testsurefire 根本不认。前面我在 POM 里已经把*IT.java加进includes了如果你没有这份配置单独跑it-tests自然就是 0。排查思路很简单打开it-tests/target/surefire-reports目录看看有没有对应的.txt报告文件。如果连报告都没有说明测试类压根没被扫描到这基本就是 include 配置的问题。5.2 坑二全量上下文启动时端口冲突在it-tests里拉起完整的 Spring Boot 上下文时经常遇到Port already in use。虽然上面的例子用了RANDOM_PORT但你项目里如果有模块在启动阶段用固定端口做了外部服务注册比如PostConstruct里启动了一个内嵌 Socket 服务RANDOM_PORT 只解决 web 容器端口解决不了这种自定义端口。排查方法先用lsof -i :端口号看哪个进程占用了端口如果发现是之前某次测试残留 JVM 进程用jps -l找到进程号清理掉。更重要的是全局禁用并行测试——在it-tests/pom.xml里把 surefire 的forkCount显式设为 1plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration forkCount1/forkCount reuseForksfalse/reuseForks /configuration /pluginreuseForksfalse虽然会让每个测试类都启动一个新 JVM测试变慢但换来了绝对的隔离性。聚合测试环境里稳定优先时间排在后面。如果项目本身已经上了 JUnit 5 并行测试那一定要把并行度调到 1否则你根本没法把责任边界划清楚。5.3 坑三嵌入式 Redis 的 jar 包冲突有些团队在业务模块里已经引入了embedded-redis然后it-tests里又声明了另一个版本。最典型的冲突表现是ClassCastException: redis.embedded.RedisServer cannot be cast to ...或者NoSuchMethodError。这个坑唯一的正解就是版本统一。把embedded-redis的版本写进父 POM 的 dependencyManagement业务模块和it-tests里都不写版本号只声明依赖。如果你的项目里有较新的业务代码用了 Lettuce 连接 Redis测试里用 Jedis 做驱动的场景也会触发版本冲突那就全项目强制统一连接池依赖。5.4 坑四测试数据残留导致第二次跑挂集成测试跟单元测试最大的不同是单元测试可以完全隔离在内存里集成测试一旦连了数据库就会有“脏数据”问题。在it-tests模块里我会写一个DataCleanerExtension实现 JUnit 5 的AfterAllCallback在每个测试类跑完后清理所有相关的表数据package com.example.it.extension; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.jdbc.core.JdbcTemplate; public class DataCleanerExtension implements AfterAllCallback { private static final String[] TABLES { orders, order_items, user_accounts, payment_records }; Override public void afterAll(ExtensionContext context) { JdbcTemplate jdbcTemplate SpringContextHolder.getBean(JdbcTemplate.class); for (String table : TABLES) { jdbcTemplate.execute(DELETE FROM table); } } }在测试基类里加上ExtendWith(DataCleanerExtension.class)这样继承这个基类的所有测试都能自动做清理。这里有个设计取舍也值得说到底该用Transactional回滚来隔离还是用物理 DELETE 清理Transactional的问题在于它只在当前线程内生效而集成测试里你经常要模拟异步消息、子线程操作这些事务不会跟着主线程回滚。物理 DELETE 的缺点是如果表里有自增主键下一次测试的 ID 不会从 1 开始但这对断言正确性没有影响真正的业务代码也不该依赖 ID 从固定值开始。所以我最后选了 DELETE 清理方案。6. 进阶玩法把聚合测试模块做成团队测试入口6.1 场景扩展要不要在这里面跑 Mutation Testing测试基建稳定之后很多团队会想进一步提升单测质量。聚合测试模块天然适合接入 PITmutation testing插件因为它掌握了全模块的测试入口做变异测试时扫描范围最完整。不过我要提醒一句PIT 在小规模模块上可以跑但如果你的项目中业务模块超过 5 个、测试类超过 200 个变异测试的耗时是几何级数增长的。我当时试点过一个 3 模块的中型项目单次跑 PIT 需要 40 分钟根本没法作为门槛放进 CI。更好的做法是只在聚合测试模块里选择核心服务类做变异测试通过targetClasses配置缩小范围plugin groupIdorg.pitest/groupId artifactIdpitest-maven/artifactId version1.9.0/version configuration targetClasses paramcom.example.servicea.*/param paramcom.example.serviceb.*/param /targetClasses targetTests paramcom.example.it.*/param /targetTests mutators mutatorCONDITIONALS_BOUNDARY/mutator mutatorINCREMENTS/mutator mutatorRETURN_VALS/mutator /mutators /configuration /plugin6.2 CI 流水线里的执行顺序建议在配套的 CI我以 Jenkins/GitLab CI 通用视角看中聚合测试模块最理想的位置是排在最后的阶段编译阶段mvn clean compile确保所有业务模块代码可编译单测阶段mvn test -DskipITs跑各业务模块内部带*Test.java的单元测试快速反馈聚合测试阶段单独执行mvn -pl it-tests -am test跑跨模块集成测试。注意第 2 步和第 3 步要分开并且第 3 步里不要再用-DskipTests这种一刀切的参数——它会把聚合模块里的测试也跳过。如果确实想要跳过聚合测试就用-DskipITs。这个参数名虽然对 surefire 来说不是标准属性但配合我前面把*IT.java也纳入 surefire includes 的配置就可以用一个简单的-DskipITstrue来单独跳过集成测试类。6.3 团队习惯如何养成技术方案落地的最后一步永远是人。聚合测试模块这套结构有个特点它对团队纪律的要求比较高。任何一个开发者在新建业务模块时如果没有把模块名注册进父 POM 的modules也没有在新模块的 POM 里放置maven-jar-plugin的 test-jar 配置聚合测试模块就无法感知到这个新模块。我建议的做法是在团队工程规范文档里把“如何新增一个模块并接入聚合测试”写成 checklist新模块的 POM 里确认继承了父 POM新模块的src/test/java里的基础工具类评估是否要上提到it-tests父 POM 的modules追加新模块IntegrationTestConfiguration的 basePackages 补上新模块的根包名跑一次全量mvn -pl it-tests -am test看上下文是否能正常拉起。你可以在it-tests里写一个简单的ContextLoadsIT它啥业务都不干只负责加载 Spring 上下文。这样只要任何一个新模块没接好全量测试立刻红问题能在合并到主干之前就暴露出来。7. 这套方案的边界与我的实际体验写到这儿也有必要说说这套方案不能帮你解决什么。如果你们的工程里业务模块超过了 20 个或者每个模块的测试上下文都很大把所有模块全部拉起来跑集成测试会让it-tests的执行时间膨胀到不可接受的范围。这时候我更推荐的做法是把聚合测试模块拆成两到三个——比如it-tests-core公共测试基建 全模块通用集成测试和it-tests-payment支付域专项集成测试——父 POM 统一管理它们的版本和顺序。我在实际推进这套方案时遇到过团队里最直接的一个质疑“每个业务模块自己跑单测不就行了为什么非要跑到一个模块里”我用一次真实的线上事故回答了这个问题之前有一个跨模块的接口签名变更service-a的方法加了参数但web模块里的调用方没同步改。因为两个模块各自测试跑的都很“绿”没有任何一个模块的测试能覆盖到这一层调用链。问题在灰度发布后爆发流量一进来就报 500 错。引入聚合测试模块之后重构service-a接口的时候web调用方的集成测试立刻红灯这层防线要比 code review 靠人眼靠谱得多。另外还有一个意外的收益因为it-tests模块里所有测试都依赖同一个IntegrationTestConfiguration开发人员在本地启动完整环境也变简单了。以前要把工程下多个模块分别启动再手工对接现在直接用测试基类里的TestRestTemplate模拟全链路请求连 Docker Compose 都不用了大部分场景用嵌入式中间件即可。最后分享一个小技巧给it-tests模块加一个README.md里面画一份简单的表格列出每个跨模块集成测试对应哪个业务链路、启动了什么外部中间件、预期验证什么行为。这个文档不是写给新手看的而是写给你自己——三个月后再回来看这个模块你会发现那些测试类名已经没法让你回忆起它们到底验证了什么业务规则。有了这张对照表维护成本会骤降。
返回列表