[{"content":"Spring Boot 集成测试框架搭建指南 🎯 目标: 提供一套通用、高效、易维护的集成测试框架，适用于任何 Spring Boot 项目\n📚 目录 快速开始 环境要求 5分钟上手 框架特点 适用场景 必需文件清单 完整目录结构 核心文件说明 框架架构 整体架构图 核心设计原则 核心组件 BaseIntegrationTest - 测试基类 TestConfig - 测试配置类 TestDataSourceConfig - 多数据源配置 TestMockConfig - Dubbo服务自动Mock配置 SqlCompatibilityInterceptor - SQL兼容拦截器 搭建步骤 添加Maven依赖 配置测试环境 准备测试数据 创建测试启动类 创建测试基类 编写测试用例 配置覆盖率检查 测试命名规范 测试类命名 测试方法命名 测试数据命名 最佳实践 测试用例设计 测试数据管理 Dubbo Mock使用原则 断言最佳实践 性能优化 测试覆盖率目标 故障排查指南 故障排查流程图 常见问题及解决方案 从现有测试迁移 迁移策略 从单元测试迁移 迁移检查清单 常见问题 H2与MySQL的SQL差异 事务回滚不生效 Mock未生效 并发测试不稳定 测试运行速度慢 附录：实施案例 项目背景 实施前的挑战 实施方案 实施效果 经验总结 总结与展望 快速参考 一、快速开始 1.1 环境要求 在开始前，请确认以下环境：\n组件 版本要求 当前项目版本 说明 JDK ≥ 1.8 1.8 必需 Spring Boot 2.x 2.1.x (企业定制) 企业定制版本 Maven ≥ 3.6 - 构建工具 H2 Database 2.x 依赖于Spring Boot版本 内存数据库 MyBatis 3.5.x 3.5.7 持久层框架 Dynamic DataSource 3.x 3.6.0 多数据源支持 Mockito Inline 4.x+ 内置 支持静态Mock Embedded Redis 0.7.3 0.7.3 内嵌Redis 1.2 5分钟上手 第一步: 添加依赖（项目已包含）\n\u0026lt;!-- Spring Boot 测试 --\u0026gt;\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;\u0026lt;/dependency\u0026gt; \u0026lt;!-- H2 内存数据库 --\u0026gt;\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.h2database\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;h2\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;\u0026lt;/dependency\u0026gt; \u0026lt;!-- Mockito Inline (支持静态Mock) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mockito\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mockito-inline\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;\u0026lt;/dependency\u0026gt; \u0026lt;!-- Embedded Redis --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;it.ozimov\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;embedded-redis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.7.3\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;\u0026lt;/dependency\u0026gt; 第二步: 创建测试基类\n@SpringBootTest(classes = TestApplication.class) @ActiveProfiles(\u0026#34;testcase\u0026#34;) public abstract class BaseIntegrationTest { // 所有集成测试继承此类 } 第三步: 编写测试\npublic class YourServiceTest extends BaseCommonServiceTest { @Autowired private YourService yourService; @Test @DisplayName(\u0026#34;测试核心业务流程\u0026#34;) void testBusinessFlow() { // 1. 准备数据 YourEntity entity = createTestEntity(); // 2. 执行操作 YourDTO result = yourService.process(entity); // 3. 验证结果 assertNotNull(result); assertEquals(expectedValue, result.getValue()); }} 1.3 框架特点 特性 说明 价值 🚀 快速启动 H2 内存数据库 + 内嵌服务 毫秒级启动，无外部依赖 🔒 测试隔离 独立测试环境 + 事务回滚 测试互不干扰，可并行执行 🎭 智能Mock 自动Mock外部依赖 减少90%的Mock代码 📊 质量可视 JaCoCo覆盖率报告 持续监控代码质量 🔧 易于维护 Builder模式 + 配置分离 降低维护成本 1.4 适用场景 ✅ 适合使用的场景:\n微服务架构的复杂系统 多数据源业务场景 依赖大量外部服务的应用 需要快速反馈的敏捷团队 强调质量的企业级项目 ⚠️ 可以简化的场景:\n简单的CRUD应用（可能过度设计） 对数据库SQL兼容性要求极高（建议使用 TestContainers） 测试执行时间要求\u0026lt;5秒的场景 二、必需文件清单 2.1 完整目录结构 基于 Call-Service 项目的实际实现，以下是完整的测试框架目录结构：\nsrc/test/ ├── java/com/yourcompany/yourapp/test/ │ ├── TestApplication.java # 测试启动类 │ ├── base/ │ │ ├── BaseIntegrationTest.java # 集成测试基类 │ │ ├── BaseCommonServiceTest.java # 服务测试基类（含数据初始化） │ ├── config/ │ │ ├── TestContextConfig.java # 测试主配置 │ │ ├── TestDataSourceConfig.java # 多数据源配置 │ │ ├── TestMockConfig.java # Dubbo Mock自动配置 │ │ └── TestRedisConfig.java # Redis配置 │ ├── interceptor/ │ │ └── SqlCompatibilityInterceptor.java # SQL兼容性拦截器 │ └── service/ │ └── XxxServiceIntegrationTest.java # 具体业务测试类 └── resources/ ├── application-testcase.properties # 测试环境配置 ├── log4j2-testcase.xml # 测试日志配置 ├── schema/ # 数据库Schema（按数据源划分） │ ├── master-schema.sql # 主库Schema │ ├── slaveone-schema.sql # 从库1 Schema │ ├── slavethree-schema.sql # 从库3 Schema │ ├── drdsmaster-schema.sql # DRDS主库Schema │ ├── cscrm-schema.sql # CS-CRM数据源Schema │ └── datawarehouse-schema.sql # 数据仓库Schema └── data/ # 初始测试数据 ├── master-data.sql # 主库测试数据 ├── drdsmaster-data.sql # DRDS测试数据 └── datawarehouse-data.sql # 数据仓库测试数据 2.2 核心文件说明 2.2.1 测试启动类 - TestApplication.java package com.yourcompany.yourapp.test; import com.yourcompany.yourapp.test.config.TestContextConfig; import org.apache.dubbo.spring.boot.autoconfigure.DubboAutoConfiguration; import org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; /** * 测试专用启动类 * 用于集成测试时启动应用 */@SpringBootApplication( scanBasePackageClasses = { TestContextConfig.class, }, exclude = { DataSourceAutoConfiguration.class, // 排除自动数据源配置 DubboAutoConfiguration.class, // 排除Dubbo自动配置 DubboRelaxedBinding2AutoConfiguration.class }) public class TestApplication { } 要点:\n排除生产环境的自动配置（DataSource、Dubbo等） 只扫描测试配置包（TestContextConfig） 避免加载不必要的Bean，加快启动速度 2.2.2 集成测试基类 - BaseIntegrationTest.java package com.yourcompany.yourapp.test.base; import static org.mockito.ArgumentMatchers.any; import com.aliyun.openservices.ons.api.ONSFactory; import com.aliyun.openservices.ons.api.Producer; import com.aliyun.openservices.ons.api.SendResult; import com.yourcompany.yourapp.test.TestApplication; import com.yourcompany.yourapp.thirdparty.oss.OSSClient; import com.yourcompany.yourapp.thirdparty.oss.OssClientFactory; import com.yourcompany.yourapp.thirdparty.oss.model.PutObjectResult; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.EventListener; import org.springframework.test.context.ActiveProfiles; import redis.embedded.RedisServer; import redis.embedded.RedisServerBuilder; @Slf4j @ActiveProfiles(\u0026#34;testcase\u0026#34;) @SpringBootTest(classes = TestApplication.class) public abstract class BaseIntegrationTest { protected static RedisServer redisServer; private static MockedStatic\u0026lt;ONSFactory\u0026gt; mockedOnsFactory; private static MockedStatic\u0026lt;OssClientFactory\u0026gt; mockedOssClientFactory; @BeforeAll public static void initBeforeClass() { log.info(\u0026#34;集成测试环境初始化开始...\u0026#34;); // Mock RocketMQ Producer mockRocketMQ(); // Mock OSS Client mockOssClient(); // 启动内嵌Redis initRedis(); // 初始化Apollo配置 initApolloConfig(); log.info(\u0026#34;集成测试环境初始化完成\u0026#34;); } /** * Mock RocketMQ Producer */ private static void mockRocketMQ() { Producer mockProducer = Mockito.mock(Producer.class); Mockito.doNothing().when(mockProducer).start(); Mockito.doNothing().when(mockProducer).shutdown(); Mockito.doReturn(new SendResult()).when(mockProducer).send(any()); mockedOnsFactory = Mockito.mockStatic(ONSFactory.class); mockedOnsFactory.when(() -\u0026gt; ONSFactory.createProducer(any())).thenReturn(mockProducer); } /** * Mock OSS Client */ private static void mockOssClient() { OSSClient mockOssClient = Mockito.mock(OSSClient.class); PutObjectResult result = new PutObjectResult(); result.setStatusCode(200); Mockito.when(mockOssClient.putObject(any())).thenReturn(result); mockedOssClientFactory = Mockito.mockStatic(OssClientFactory.class); mockedOssClientFactory.when(() -\u0026gt; OssClientFactory.getOSSClient()).thenReturn(mockOssClient); } /** * 启动内嵌Redis服务 */ protected static void initRedis() { redisServer = new RedisServerBuilder() .bind(\u0026#34;localhost\u0026#34;) .port(6380) .setting(\u0026#34;requirepass \u0026lt;password\u0026gt;\u0026#34;) .build(); redisServer.start(); log.info(\u0026#34;内嵌Redis启动成功，端口: 6380\u0026#34;); } /** * 初始化Apollo配置（本地模式） */ protected static void initApolloConfig() { System.setProperty(\u0026#34;app.id\u0026#34;, \u0026#34;your-app-name\u0026#34;); System.setProperty(\u0026#34;apollo.cacheDir\u0026#34;, \u0026#34;src/test/resources\u0026#34;); System.setProperty(\u0026#34;apollo.meta\u0026#34;, \u0026#34;http://none\u0026#34;); System.setProperty(\u0026#34;env\u0026#34;, \u0026#34;local\u0026#34;); System.setProperty(\u0026#34;apollo.bootstrap.enabled\u0026#34;, \u0026#34;true\u0026#34;); log.info(\u0026#34;Apollo配置初始化完成\u0026#34;); } @AfterAll public static void afterAll() { // 关闭静态Mock if (mockedOnsFactory != null) { mockedOnsFactory.close(); } if (mockedOssClientFactory != null) { mockedOssClientFactory.close(); } } /** * 监听Spring上下文关闭事件，停止Redis */ @EventListener public void listener(ContextClosedEvent closedEvent) { if (redisServer != null \u0026amp;\u0026amp; redisServer.isActive()) { redisServer.stop(); log.info(\u0026#34;内嵌Redis已停止\u0026#34;); } }} 要点:\n使用 @BeforeAll 初始化共享资源（Redis、静态Mock） 使用 MockedStatic Mock静态工厂类（ONSFactory、OssClientFactory） 使用 @EventListener 监听上下文关闭事件，优雅停止Redis 必须在 @AfterAll 中关闭 MockedStatic，否则会影响其他测试 2.2.3 服务测试基类 - BaseCommonServiceTest.java package com.yourcompany.yourapp.test.base; import com.yourcompany.yourapp.test.config.TestDataSourceConfig; import java.util.Map; import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; /** * 服务测试基类 * 自动初始化多个数据源的Schema和测试数据 */@Slf4j public abstract class BaseCommonServiceTest extends BaseIntegrationTest implements InitializingBean { @Autowired(required = false) private Map\u0026lt;String, DataSource\u0026gt; dataSourceMap; @Override public void afterPropertiesSet() throws Exception { doInitDatasource(); } /** * 初始化所有数据源的Schema和测试数据 * 使用单例标志位，确保只初始化一次 */ void doInitDatasource() { if (TestDataSourceConfig.isDatasourceInitialized()) { return; } log.info(\u0026#34;开始初始化测试数据库...\u0026#34;); // 初始化各个数据源 initDatabase(\u0026#34;dataSourceMaster\u0026#34;, \u0026#34;schema/master-schema.sql\u0026#34;, \u0026#34;data/master-data.sql\u0026#34;); initDatabase(\u0026#34;dataSourceSlaveOne\u0026#34;, \u0026#34;schema/slaveone-schema.sql\u0026#34;, \u0026#34;\u0026#34;); initDatabase(\u0026#34;dataSourceSlaveTwo\u0026#34;, \u0026#34;schema/master-schema.sql\u0026#34;, \u0026#34;data/master-data.sql\u0026#34;); initDatabase(\u0026#34;dataSourceSlaveThree\u0026#34;, \u0026#34;schema/slavethree-schema.sql\u0026#34;, \u0026#34;\u0026#34;); initDatabase(\u0026#34;dataSourceDrdsMaster\u0026#34;, \u0026#34;schema/drdsmaster-schema.sql\u0026#34;, \u0026#34;data/drdsmaster-data.sql\u0026#34;); initDatabase(\u0026#34;dataSourceCsCrm\u0026#34;, \u0026#34;schema/cscrm-schema.sql\u0026#34;, \u0026#34;\u0026#34;); initDatabase(\u0026#34;dataSourceWareHouse\u0026#34;, \u0026#34;schema/datawarehouse-schema.sql\u0026#34;, \u0026#34;data/datawarehouse-data.sql\u0026#34;); TestDataSourceConfig.setDatasourceInitialized(); log.info(\u0026#34;测试数据库初始化完成\u0026#34;); } /** * 初始化指定数据源的表结构和数据 * @param dataSourceKey 数据源标识 * @param schemaLocation schema SQL文件路径 * @param dataLocation 数据SQL文件路径（可为空） */ protected void initDatabase(String dataSourceKey, String schemaLocation, String dataLocation) { if (dataSourceMap == null) { log.warn(\u0026#34;dataSourceMap为空，无法初始化数据库\u0026#34;); return; } DataSource dataSource = dataSourceMap.get(dataSourceKey); if (dataSource == null) { throw new IllegalArgumentException(\u0026#34;未找到数据源: \u0026#34; + dataSourceKey); } ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); if (schemaLocation != null \u0026amp;\u0026amp; !schemaLocation.isEmpty()) { populator.addScript(new ClassPathResource(schemaLocation)); } if (dataLocation != null \u0026amp;\u0026amp; !dataLocation.isEmpty()) { populator.addScript(new ClassPathResource(dataLocation)); } try { log.info(\u0026#34;初始化数据源 [{}]\u0026#34;, dataSourceKey); populator.execute(dataSource); } catch (Exception e) { throw new RuntimeException(\u0026#34;初始化数据库失败: \u0026#34; + dataSourceKey, e); } }} 要点:\n继承自 BaseIntegrationTest，复用基础设施 实现 InitializingBean，在Bean初始化后自动执行数据库初始化 使用单例标志位避免重复初始化（Spring容器复用时） 支持多数据源批量初始化 2.2.4 测试配置文件 - application-testcase.properties # 测试环境配置 spring.application.name=your-app-name server.port=0 # H2数据库配置 - 主数据源 spring.datasource.druid.url=jdbc:h2:mem:master;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.username=sa spring.datasource.druid.password= spring.datasource.druid.driver-class-name=org.h2.Driver spring.datasource.druid.initial-size=1 spring.datasource.druid.min-idle=1 spring.datasource.druid.max-active=5 # H2数据库配置 - 从数据源1 spring.datasource.druid.slave-one.url=jdbc:h2:mem:slave1;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.slave-one.username=sa spring.datasource.druid.slave-one.password= spring.datasource.druid.slave-one.driver-class-name=org.h2.Driver # H2数据库配置 - 从数据源2（master的只读副本） spring.datasource.druid.slave-two.url=jdbc:h2:mem:slave2;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.slave-two.username=sa spring.datasource.druid.slave-two.password= # H2数据库配置 - 从数据源3 spring.datasource.druid.slave-three.url=jdbc:h2:mem:slave3;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.slave-three.username=sa spring.datasource.druid.slave-three.password= # H2数据库配置 - DRDS主数据源 spring.datasource.druid.drds-master.url=jdbc:h2:mem:drds;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.drds-master.username=sa spring.datasource.druid.drds-master.password= # H2数据库配置 - CS-CRM数据源 spring.datasource.druid.cs-crm.url=jdbc:h2:mem:cs_crm;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.cs-crm.username=sa spring.datasource.druid.cs-crm.password= # H2数据库配置 - 数据仓库 spring.datasource.druid.warehouse.url=jdbc:h2:mem:warehouse;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.warehouse.username=sa spring.datasource.druid.warehouse.password= # Redis配置（连接内嵌Redis） redis.hostname=localhost redis.port=6380 redis.password=\u0026lt;your-password\u0026gt; redis.dbIndex=0 # 禁用Dubbo注册中心 dubbo.registry.address=N/A dubbo.consumer.check=false dubbo.registry.check=false # 日志配置 logging.level.root=INFO logging.level.com.yourcompany.yourapp=DEBUG mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl H2 URL参数说明:\nMODE=MySQL: H2兼容MySQL语法模式 DB_CLOSE_DELAY=-1: 保持数据库打开直到JVM关闭 DATABASE_TO_LOWER=TRUE: 表名/字段名自动转小写 三、框架架构 2.1 整体架构图 ┌─────────────────────────────────────────────────────────────┐ │ 业务测试层 ││ ┌──────────────────────────────────────────────────────┐ │ │ │ XxxServiceIntegrationTest (具体业务测试) │ │ │ │ - 全链路业务流程测试 │ ││ │ - 异常场景测试 │ ││ │ - 边界条件测试 │ ││ └──────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ 测试基类层 ││ ┌──────────────────────────────────────────────────────┐ │ │ │ BaseIntegrationTest │ │ │ │ - Spring Boot 测试环境 │ ││ │ - 内嵌服务管理 (Redis/MQ) │ ││ │ - Mock配置 │ ││ │ - 生命周期管理 │ ││ └──────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ 测试配置层 ││ ┌────────────────┬────────────────┬───────────────────┐ │ │ │ TestAppConfig │TestDataSource │ TestMockConfig │ │ │ │ (主配置) │ (数据源配置) │ (Mock配置) │ │ │ └────────────────┴────────────────┴───────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ 基础设施层 ││ ┌────────────────┬────────────────┬───────────────────┐ │ │ │ H2 Database │ Embedded Redis │ Static Mock │ │ │ │ (内存数据库) │ (内嵌缓存) │ (静态服务Mock) │ │ │ └────────────────┴────────────────┴───────────────────┘ │ └─────────────────────────────────────────────────────────────┘ 2.2 核心设计原则 2.2.1 测试金字塔原则 /\\ / \\ E2E 测试 (10%) /____\\ 关键用户路径 / \\/ 集成测试 \\ (20%) / 核心流程 \\ /______________\\ / \\ / 单元测试 (70%) \\ 单个类/方法 /__________________\\ 实践建议:\n单元测试: 覆盖所有公共方法、边界条件、异常处理 集成测试: 覆盖核心业务流程、多组件协作场景 E2E测试: 仅覆盖最关键的用户路径 2.2.2 测试隔离原则 环境隔离:\n// 独立的测试启动类 @SpringBootApplication( scanBasePackageClasses = {TestConfig.class}, exclude = {DataSourceAutoConfiguration.class}) public class TestApplication {} 数据隔离:\n// 事务自动回滚 @Transactional @Rollback @Test void testCreate() { service.create(entity); // 测试结束自动回滚 } Mock隔离:\n// 每个测试独立配置Mock @BeforeEach void setUp() { ExternalService mock = getMock(ExternalService.class); when(mock.call()).thenReturn(testData);} 四、核心组件 4.1 BaseIntegrationTest - 测试基类 职责: 提供测试运行的基础环境和生命周期管理\n核心功能:\nSpring Boot 测试环境启动 内嵌服务管理（Redis、MQ等） 静态服务Mock（工厂类、配置类） 资源生命周期管理 实现模板:\n@SpringBootTest(classes = TestApplication.class) @ActiveProfiles(\u0026#34;test\u0026#34;) public abstract class BaseIntegrationTest { protected static EmbeddedRedis redisServer; protected static MockedStatic\u0026lt;StaticFactory\u0026gt; mockedFactory; @BeforeAll public static void initSharedResources() { // 启动内嵌服务 startEmbeddedServices(); // Mock静态类 mockStaticDependencies(); } @AfterAll public static void cleanupSharedResources() { // 关闭Mock if (mockedFactory != null) { mockedFactory.close(); } } @EventListener(ContextClosedEvent.class) public void onContextClosed() { // 停止内嵌服务 if (redisServer != null) { redisServer.stop(); } } private static void startEmbeddedServices() { // 实现内嵌服务启动逻辑 } private static void mockStaticDependencies() { // 实现静态Mock逻辑 }} 4.2 TestConfig - 测试配置类 职责: 配置测试环境所需的Spring Bean\n核心配置项:\n@Configuration @ComponentScan( basePackages = \u0026#34;com.yourcompany.yourapp\u0026#34;, excludeFilters = @ComponentScan.Filter( type = FilterType.REGEX, pattern = \u0026#34;.*ProductionConfig\u0026#34; )) @MapperScan(\u0026#34;com.yourcompany.yourapp.mapper\u0026#34;) public class TestConfig { // Redis配置 @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(\u0026#34;localhost\u0026#34;, 6380); } // RestTemplate配置 @Bean public RestTemplate restTemplate() { return new RestTemplate(); } // MyBatis拦截器（兼容H2） @Bean public SqlCompatibilityInterceptor sqlInterceptor() { return new SqlCompatibilityInterceptor(); }} 4.3 TestDataSourceConfig - 多数据源配置 职责: 配置多个H2内存数据库，模拟生产环境的多数据源架构\nCall-Service项目的多数据源架构:\nmaster: 主库（读写） slave-one: 从库1 slave-two: 从库2（master的只读副本） slave-three: 从库3 drds-master: DRDS主库 cs-crm: CS-CRM数据源 warehouse: 数据仓库 完整配置实现:\npackage com.yourcompany.yourapp.test.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class TestDataSourceConfig { private static volatile boolean datasourceInitialized = false; public static boolean isDatasourceInitialized() { return datasourceInitialized; } public static void setDatasourceInitialized() { datasourceInitialized = true; } /** * 主数据源 */ @Bean(name = \u0026#34;dataSourceMaster\u0026#34;) @Primary @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid\u0026#34;) public DataSource dataSourceMaster() { return new DruidDataSource(); } /** * 从数据源1 */ @Bean(name = \u0026#34;dataSourceSlaveOne\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.slave-one\u0026#34;) public DataSource dataSourceSlaveOne() { return new DruidDataSource(); } /** * 从数据源2（master的只读副本） */ @Bean(name = \u0026#34;dataSourceSlaveTwo\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.slave-two\u0026#34;) public DataSource dataSourceSlaveTwo() { return new DruidDataSource(); } /** * 从数据源3 */ @Bean(name = \u0026#34;dataSourceSlaveThree\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.slave-three\u0026#34;) public DataSource dataSourceSlaveThree() { return new DruidDataSource(); } /** * DRDS主数据源 */ @Bean(name = \u0026#34;dataSourceDrdsMaster\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.drds-master\u0026#34;) public DataSource dataSourceDrdsMaster() { return new DruidDataSource(); } /** * CS-CRM数据源 */ @Bean(name = \u0026#34;dataSourceCsCrm\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.cs-crm\u0026#34;) public DataSource dataSourceCsCrm() { return new DruidDataSource(); } /** * 数据仓库数据源 */ @Bean(name = \u0026#34;dataSourceWareHouse\u0026#34;) @ConfigurationProperties(prefix = \u0026#34;spring.datasource.druid.warehouse\u0026#34;) public DataSource dataSourceWareHouse() { return new DruidDataSource(); } /** * 注入所有数据源到Map，方便BaseCommonServiceTest批量初始化 */ @Bean public Map\u0026lt;String, DataSource\u0026gt; dataSourceMap( DataSource dataSourceMaster, DataSource dataSourceSlaveOne, DataSource dataSourceSlaveTwo, DataSource dataSourceSlaveThree, DataSource dataSourceDrdsMaster, DataSource dataSourceCsCrm, DataSource dataSourceWareHouse ) { Map\u0026lt;String, DataSource\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;dataSourceMaster\u0026#34;, dataSourceMaster); map.put(\u0026#34;dataSourceSlaveOne\u0026#34;, dataSourceSlaveOne); map.put(\u0026#34;dataSourceSlaveTwo\u0026#34;, dataSourceSlaveTwo); map.put(\u0026#34;dataSourceSlaveThree\u0026#34;, dataSourceSlaveThree); map.put(\u0026#34;dataSourceDrdsMaster\u0026#34;, dataSourceDrdsMaster); map.put(\u0026#34;dataSourceCsCrm\u0026#34;, dataSourceCsCrm); map.put(\u0026#34;dataSourceWareHouse\u0026#34;, dataSourceWareHouse); return map; }} 配置要点:\n使用Druid连接池: 与生产环境保持一致 @Primary注解: 标记主数据源，避免注入歧义 单例标志位: 防止多次初始化（Spring容器复用时） dataSourceMap: 方便测试基类批量初始化所有数据源 对应的配置文件:\n# 主数据源 spring.datasource.druid.url=jdbc:h2:mem:master;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.username=sa spring.datasource.druid.password= spring.datasource.druid.driver-class-name=org.h2.Driver spring.datasource.druid.initial-size=1 spring.datasource.druid.min-idle=1 spring.datasource.druid.max-active=5 # 从数据源1 spring.datasource.druid.slave-one.url=jdbc:h2:mem:slave1;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.druid.slave-one.username=sa spring.datasource.druid.slave-one.password= spring.datasource.druid.slave-one.driver-class-name=org.h2.Driver spring.datasource.druid.slave-one.initial-size=1 spring.datasource.druid.slave-one.min-idle=1 spring.datasource.druid.slave-one.max-active=5 # ... 其他数据源配置类似 4.4 TestMockConfig - Dubbo服务自动Mock配置 职责: 自动扫描并Mock所有Dubbo远程服务（@Reference注解）\n实现原理: 使用Spring的 BeanPostProcessor 在Bean初始化时扫描字段，发现 @Reference 注解后注入Mock对象\n完整实现:\npackage com.yourcompany.yourapp.test.config; import org.apache.dubbo.config.annotation.Reference; import org.mockito.Mockito; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Dubbo服务自动Mock配置 * 扫描所有@Reference注解的字段，自动注入Mock对象 */@Component public class TestMockConfig implements BeanPostProcessor { /** * Mock对象注册表 * Key: 接口Class, Value: Mock对象实例 */ private static final Map\u0026lt;Class\u0026lt;?\u0026gt;, Object\u0026gt; mockRegistry = new ConcurrentHashMap\u0026lt;\u0026gt;(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { // 扫描@Reference注解（Dubbo服务） if (field.isAnnotationPresent(Reference.class)) { mockAndInjectDubboService(bean, field); } } return bean; } /** * Mock Dubbo服务并注入 */ private void mockAndInjectDubboService(Object bean, Field field) { try { Class\u0026lt;?\u0026gt; serviceInterface = field.getType(); // 从注册表获取或创建Mock对象 Object mockObject = mockRegistry.computeIfAbsent( serviceInterface, clazz -\u0026gt; Mockito.mock(clazz) ); // 注入Mock对象到字段 field.setAccessible(true); field.set(bean, mockObject); } catch (Exception e) { throw new RuntimeException(\u0026#34;Mock Dubbo服务失败: \u0026#34; + field.getName(), e); } } /** * 获取Mock对象，用于测试中配置Mock行为 ** @param clazz 服务接口Class * @return Mock对象 */ public static \u0026lt;T\u0026gt; T getMock(Class\u0026lt;T\u0026gt; clazz) { Object mock = mockRegistry.get(clazz); if (mock == null) { throw new IllegalStateException(\u0026#34;Mock对象未注册: \u0026#34; + clazz.getName()); } return (T) mock; } /** * 清空Mock注册表（仅用于特殊场景） */ public static void clearMockRegistry() { mockRegistry.clear(); }} 使用示例:\n@SpringBootTest public class OrderServiceIntegrationTest extends BaseCommonServiceTest { @Autowired private OrderService orderService; // 被测试的服务 // OrderService内部依赖的Dubbo服务会被自动Mock @Test @DisplayName(\u0026#34;测试订单创建（依赖外部用户服务）\u0026#34;) void testCreateOrder() { // 1. 获取自动Mock的Dubbo服务 UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); // 2. 配置Mock行为 when(userService.getUserById(anyLong())).thenReturn(buildMockUser()); // 3. 执行业务操作 OrderDTO order = orderService.createOrder(buildOrderRequest()); // 4. 验证结果 assertNotNull(order.getId()); assertEquals(\u0026#34;PENDING\u0026#34;, order.getStatus()); // 5. 验证Dubbo服务被调用 verify(userService, times(1)).getUserById(anyLong()); }} 优势:\n✅ 自动化: 无需手动Mock每个Dubbo服务 ✅ 集中管理: 所有Mock对象在注册表中统一管理 ✅ 易于配置: 通过 getMock() 获取Mock对象后即可配置行为 ✅ 单例模式: 同一接口的Mock对象在所有Bean中共享 注意事项:\nMock对象在整个测试类生命周期中共享，需要在 @BeforeEach 中重置Mock行为 如果需要不同测试使用不同Mock行为，需要在每个测试方法中重新配置 4.5 SqlCompatibilityInterceptor - SQL兼容拦截器 职责: 兼容MySQL特有语法，使其能在H2上运行\n常见兼容场景:\n移除 FORCE INDEX(...) 提示 转换日期函数 处理分页语法差异 @Intercepts({ @Signature( type = StatementHandler.class, method = \u0026#34;prepare\u0026#34;, args = {Connection.class, Integer.class} )}) public class SqlCompatibilityInterceptor implements Interceptor { private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile(\u0026#34;FORCE\\\\s+INDEX\\\\s*\\\\([^)]*\\\\)\u0026#34;, Pattern.CASE_INSENSITIVE); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); String originalSql = boundSql.getSql(); // 兼容处理 String compatibleSql = makeCompatible(originalSql); // 更新SQL Field sqlField = boundSql.getClass().getDeclaredField(\u0026#34;sql\u0026#34;); sqlField.setAccessible(true); sqlField.set(boundSql, compatibleSql); return invocation.proceed(); } private String makeCompatible(String sql) { // 移除FORCE INDEX sql = FORCE_INDEX_PATTERN.matcher(sql).replaceAll(\u0026#34;\u0026#34;); // 其他兼容处理... return sql; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); }} 五、搭建步骤 步骤 1: 添加Maven依赖 \u0026lt;dependencies\u0026gt; \u0026lt;!-- Spring Boot 测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- H2 内存数据库 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.h2database\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;h2\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Mockito (支持静态Mock) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mockito\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mockito-inline\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.11.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- AssertJ (流式断言) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.assertj\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;assertj-core\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 内嵌Redis (可选) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;it.ozimov\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;embedded-redis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.7.3\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- TestContainers (可选，用于真实数据库) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.testcontainers\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.19.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt;\u0026lt;/dependencies\u0026gt; 步骤 2: 配置测试环境 创建 application-test.properties:\n# H2 数据库配置 spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1 spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # H2 控制台 (可选，用于调试) spring.h2.console.enabled=true spring.h2.console.path=/h2-console # Redis 配置 (内嵌) spring.redis.host=localhost spring.redis.port=6380 spring.redis.password=\u0026lt;your-password\u0026gt; # MyBatis 配置 mybatis.configuration.map-underscore-to-camel-case=true mybatis.mapper-locations=classpath*:mapper/**/*.xml # 日志配置 logging.level.root=INFO logging.level.com.yourcompany=DEBUG logging.level.org.springframework.test=INFO # 关闭不需要的自动配置 spring.autoconfigure.exclude=\\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 步骤 3: 准备测试数据 创建 Schema 文件: src/test/resources/schema.sql\n-- 用户表 CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100), status VARCHAR(20), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username), INDEX idx_status (status)); -- 订单表 CREATE TABLE IF NOT EXISTS orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, order_no VARCHAR(50) NOT NULL UNIQUE, amount DECIMAL(10, 2), status VARCHAR(20), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_order_no (order_no)); 创建 Data 文件: src/test/resources/data.sql\n-- 插入测试用户 INSERT INTO users (username, email, status) VALUES (\u0026#39;testuser1\u0026#39;, \u0026#39;test1@example.com\u0026#39;, \u0026#39;ACTIVE\u0026#39;), (\u0026#39;testuser2\u0026#39;, \u0026#39;test2@example.com\u0026#39;, \u0026#39;ACTIVE\u0026#39;); -- 插入测试订单 INSERT INTO orders (user_id, order_no, amount, status) VALUES (1, \u0026#39;ORDER001\u0026#39;, 100.00, \u0026#39;PENDING\u0026#39;), (1, \u0026#39;ORDER002\u0026#39;, 200.00, \u0026#39;COMPLETED\u0026#39;); 步骤 4: 创建测试启动类 package com.yourcompany.yourapp; @SpringBootApplication( scanBasePackageClasses = {TestConfig.class}, exclude = { DataSourceAutoConfiguration.class, // 排除其他不需要的自动配置 }) public class TestApplication { // 仅作为测试启动入口，无需main方法 } 步骤 5: 创建测试基类 package com.yourcompany.yourapp.integration; @SpringBootTest(classes = TestApplication.class) @ActiveProfiles(\u0026#34;test\u0026#34;) public abstract class BaseIntegrationTest { protected static RedisServer redisServer; @BeforeAll public static void init() { startEmbeddedRedis(); mockStaticServices(); } @AfterAll public static void cleanup() { // 清理资源 } private static void startEmbeddedRedis() { try { redisServer = RedisServer.builder() .port(6380) .setting(\u0026#34;requirepass \u0026lt;password\u0026gt;\u0026#34;) .build(); redisServer.start(); } catch (Exception e) { throw new RuntimeException(\u0026#34;Redis启动失败\u0026#34;, e); } } private static void mockStaticServices() { // Mock静态工厂类等 }} 步骤 6: 编写测试用例 package com.yourcompany.yourapp.service; @Transactional @Rollback public class UserServiceTest extends BaseIntegrationTest { @Autowired private UserService userService; @Test @DisplayName(\u0026#34;测试用户创建流程\u0026#34;) void testCreateUser() { // 准备测试数据 UserCreateRequest request = UserCreateRequest.builder() .username(\u0026#34;newuser\u0026#34;) .email(\u0026#34;new@example.com\u0026#34;) .build(); // 执行业务操作 UserDTO result = userService.createUser(request); // 验证结果 assertNotNull(result.getId()); assertEquals(\u0026#34;newuser\u0026#34;, result.getUsername()); assertEquals(\u0026#34;ACTIVE\u0026#34;, result.getStatus()); } @Test @DisplayName(\u0026#34;测试重复用户名抛出异常\u0026#34;) void testDuplicateUsername() { UserCreateRequest request = UserCreateRequest.builder() .username(\u0026#34;testuser1\u0026#34;) // 已存在 .email(\u0026#34;duplicate@example.com\u0026#34;) .build(); assertThrows(DuplicateUserException.class, () -\u0026gt; { userService.createUser(request); }); }} 步骤 7: 配置覆盖率检查 添加 JaCoCo 插件:\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.jacoco\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jacoco-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.8.11\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;prepare-agent\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;report\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;test\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;report\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;check\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;check\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;rules\u0026gt; \u0026lt;rule\u0026gt; \u0026lt;element\u0026gt;BUNDLE\u0026lt;/element\u0026gt; \u0026lt;limits\u0026gt; \u0026lt;limit\u0026gt; \u0026lt;counter\u0026gt;LINE\u0026lt;/counter\u0026gt; \u0026lt;value\u0026gt;COVEREDRATIO\u0026lt;/value\u0026gt; \u0026lt;minimum\u0026gt;0.80\u0026lt;/minimum\u0026gt; \u0026lt;/limit\u0026gt; \u0026lt;/limits\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;/rules\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt;\u0026lt;/plugin\u0026gt; 运行测试并查看覆盖率:\n# 执行测试 mvn clean test # 查看覆盖率报告 open target/site/jacoco/index.html 六、测试命名规范 6.1 测试类命名 // ✅ 推荐: [被测试类名] + IntegrationTest OrderServiceIntegrationTest.java UserServiceIntegrationTest.java // ✅ DAO层测试: [被测试类名] + Test OrderMapperTest.java // ❌ 避免 TestOrderService.java // Test前缀不推荐 OrderServiceTests.java // 多余的s OrderServiceIT.java // IT缩写不够清晰 6.2 测试方法命名 推荐方式: 中文@DisplayName（业务测试优先）\n@Test @DisplayName(\u0026#34;创建订单-成功场景\u0026#34;) void testCreateOrder() { // ...} @Test @DisplayName(\u0026#34;创建订单-订单号重复时抛出异常\u0026#34;) void testCreateOrderWithDuplicateOrderNo() { // ...} @Test @DisplayName(\u0026#34;导入数据-批量导入1000条数据\u0026#34;) void testImportData() { // ...} 备选方式: should_When模式（技术组件测试）\n@Test @DisplayName(\u0026#34;should_ReturnUser_When_ValidIdProvided\u0026#34;) void shouldReturnUserWhenValidIdProvided() { // ...} @Test @DisplayName(\u0026#34;should_ThrowException_When_UserNotFound\u0026#34;) void shouldThrowExceptionWhenUserNotFound() { // ...} 测试方法命名原则:\n方法名使用驼峰命名，以 test 开头 使用 @DisplayName 提供中文业务描述 一个测试方法只测试一个场景 测试名称要体现测试意图和预期结果 6.3 测试数据命名 // ✅ 推荐: build + 实体名 private OrderDTO buildOrder() { return OrderDTO.builder() .orderName(\u0026#34;测试订单_\u0026#34; + System.currentTimeMillis()) .build();} // ✅ 推荐: create + 实体名（需要持久化） private OrderDTO createOrder() { OrderDTO order = buildOrder(); orderService.create(order); return order;} // ✅ 推荐: mock + 服务名 + 方法名 private void mockUserServiceGetUser() { UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); when(mock.getUserById(anyLong())).thenReturn(buildMockUser());} 七、最佳实践 7.1 测试用例设计 7.1.1 全链路集成测试 目的: 模拟真实业务流程，验证多个组件协作\n@Test @DisplayName(\u0026#34;订单全链路集成测试\u0026#34;) @Transactional @Rollback void testOrderFullFlow() { // 1. 创建用户 UserDTO user = userService.createUser(buildUserRequest()); // 2. 创建订单 OrderDTO order = orderService.createOrder(user.getId(), buildOrderRequest()); assertNotNull(order.getOrderNo()); assertEquals(\u0026#34;PENDING\u0026#34;, order.getStatus()); // 3. 支付订单 PaymentResult payment = orderService.pay(order.getId(), buildPaymentRequest()); assertTrue(payment.isSuccess()); // 4. 验证订单状态 OrderDTO paidOrder = orderService.getById(order.getId()); assertEquals(\u0026#34;PAID\u0026#34;, paidOrder.getStatus()); // 5. 发货 orderService.ship(order.getId()); // 6. 完成订单 OrderDTO completedOrder = orderService.getById(order.getId()); assertEquals(\u0026#34;COMPLETED\u0026#34;, completedOrder.getStatus());} 7.1.2 参数化测试 目的: 用一个测试方法覆盖多种场景\n@ParameterizedTest @DisplayName(\u0026#34;测试各种无效邮箱格式\u0026#34;) @ValueSource(strings = { \u0026#34;invalid\u0026#34;, \u0026#34;invalid@\u0026#34;, \u0026#34;@example.com\u0026#34;, \u0026#34;invalid@.com\u0026#34;, \u0026#34;\u0026#34;}) void testInvalidEmail(String invalidEmail) { UserCreateRequest request = UserCreateRequest.builder() .username(\u0026#34;testuser\u0026#34;) .email(invalidEmail) .build(); assertThrows(ValidationException.class, () -\u0026gt; { userService.createUser(request); });} @ParameterizedTest @DisplayName(\u0026#34;测试不同订单金额的手续费计算\u0026#34;) @CsvSource({ \u0026#34;100.00, 1.00\u0026#34;, // 金额, 预期手续费 \u0026#34;1000.00, 10.00\u0026#34;, \u0026#34;10000.00, 100.00\u0026#34;}) void testOrderFeeCalculation(BigDecimal amount, BigDecimal expectedFee) { OrderDTO order = orderService.createOrder(userId, amount); assertEquals(expectedFee, order.getFee());} 7.1.3 异常场景测试 目的: 验证系统的异常处理能力\n@Test @DisplayName(\u0026#34;测试外部服务失败时的降级处理\u0026#34;) void testExternalServiceFailure() { // Mock外部服务抛出异常 PaymentService paymentMock = TestMockConfig.getMock(PaymentService.class); when(paymentMock.pay(any())) .thenThrow(new RemoteServiceException(\u0026#34;支付服务不可用\u0026#34;)); // 执行业务操作 OrderDTO order = orderService.createOrder(userId, buildOrderRequest()); PaymentResult result = orderService.pay(order.getId(), buildPaymentRequest()); // 验证降级处理 assertFalse(result.isSuccess()); assertEquals(\u0026#34;PAYMENT_SERVICE_ERROR\u0026#34;, result.getErrorCode()); // 验证订单状态未变化 OrderDTO unchangedOrder = orderService.getById(order.getId()); assertEquals(\u0026#34;PENDING\u0026#34;, unchangedOrder.getStatus());} 7.2 测试数据管理 7.2.1 Builder模式构建测试数据 public class TestDataBuilder { public static UserCreateRequestBuilder userRequest() { return new UserCreateRequestBuilder(); } public static OrderCreateRequestBuilder orderRequest() { return new OrderCreateRequestBuilder(); } public static class UserCreateRequestBuilder { private String username = \u0026#34;testuser\u0026#34;; private String email = \u0026#34;test@example.com\u0026#34;; private String status = \u0026#34;ACTIVE\u0026#34;; public UserCreateRequestBuilder username(String username) { this.username = username; return this; } public UserCreateRequestBuilder email(String email) { this.email = email; return this; } public UserCreateRequest build() { return UserCreateRequest.builder() .username(username + \u0026#34;_\u0026#34; + System.currentTimeMillis()) .email(email) .status(status) .build(); } }} // 使用示例 @Test void testCreateUser() { UserCreateRequest request = TestDataBuilder.userRequest() .username(\u0026#34;customuser\u0026#34;) .email(\u0026#34;custom@example.com\u0026#34;) .build(); UserDTO result = userService.createUser(request); assertNotNull(result);} 7.2.2 使用@Sql初始化数据 @Test @DisplayName(\u0026#34;测试查询大量订单的性能\u0026#34;) @Sql(scripts = \u0026#34;/test-data/large-orders.sql\u0026#34;) void testQueryLargeOrders() { List\u0026lt;OrderDTO\u0026gt; orders = orderService.queryByUserId(userId); assertTrue(orders.size() \u0026gt;= 1000);} @Test @Sql(scripts = { \u0026#34;/test-data/setup-users.sql\u0026#34;, \u0026#34;/test-data/setup-orders.sql\u0026#34;}) @Sql(scripts = \u0026#34;/test-data/cleanup.sql\u0026#34;, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) void testWithSpecificData() { // 测试逻辑 } 7.3 Dubbo Mock使用原则 7.3.1 什么应该Mock ✅ 应该Mock:\nDubbo远程服务（@Reference注解的服务） 外部HTTP接口 第三方服务（OSS、短信、邮件） RocketMQ Producer 难以构造的对象 不稳定的依赖 ❌ 不应该Mock:\n被测试的核心业务逻辑 简单的POJO对象 数据访问层（Mapper/DAO）- 用内存数据库真实测试 Spring容器管理的Bean（Service、Component） Call-Service项目中需要Mock的组件:\n// 1. Dubbo远程服务（自动Mock） @Reference private UserRemoteService userRemoteService; // 2. 静态工厂类（BaseIntegrationTest中已Mock） ONSFactory.createProducer(...) // RocketMQ OssClientFactory.getOSSClient() // 阿里云OSS // 3. Apollo配置中心（BaseIntegrationTest中已配置本地模式） 7.3.2 动态配置Dubbo Mock行为 @Test @DisplayName(\u0026#34;测试调用用户服务-不同响应场景\u0026#34;) void testCallUserService() { // 获取自动Mock的Dubbo服务 UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); // 场景1: 正常返回用户 UserDTO mockUser = UserDTO.builder() .id(1001L) .username(\u0026#34;testuser\u0026#34;) .build(); when(userService.getUserById(1001L)).thenReturn(mockUser); // 场景2: 用户不存在返回null when(userService.getUserById(9999L)).thenReturn(null); // 场景3: 服务异常抛出RpcException when(userService.getUserById(0L)) .thenThrow(new RpcException(\u0026#34;服务不可用\u0026#34;)); // 执行测试 UserDTO result1 = orderService.getUser(1001L); assertNotNull(result1); UserDTO result2 = orderService.getUser(9999L); assertNull(result2); assertThrows(RpcException.class, () -\u0026gt; { orderService.getUser(0L); });} 7.3.3 验证Dubbo服务调用 @Test @DisplayName(\u0026#34;测试创建订单时调用用户服务验证用户\u0026#34;) void testCreateOrderCallsUserService() { UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); // 配置Mock返回 when(userService.getUserById(anyLong())).thenReturn(buildMockUser()); // 执行业务操作 OrderDTO order = orderService.createOrder(1001L, buildOrderRequest()); // 验证Dubbo服务被正确调用 ArgumentCaptor\u0026lt;Long\u0026gt; userIdCaptor = ArgumentCaptor.forClass(Long.class); verify(userService, times(1)).getUserById(userIdCaptor.capture()); // 验证调用参数 assertEquals(1001L, userIdCaptor.getValue());} 7.3.4 Mock重置（避免测试间干扰） @SpringBootTest public class OrderServiceIntegrationTest extends BaseCommonServiceTest { private UserRemoteService userService; private PaymentRemoteService paymentService; @BeforeEach void setUp() { // 获取Mock对象 userService = TestMockConfig.getMock(UserRemoteService.class); paymentService = TestMockConfig.getMock(PaymentRemoteService.class); // 重置Mock状态（清除之前测试的配置） Mockito.reset(userService, paymentService); // 配置通用Mock行为 when(userService.getUserById(anyLong())).thenReturn(buildDefaultUser()); } @Test @DisplayName(\u0026#34;测试场景1\u0026#34;) void testScenario1() { // 特定场景的Mock配置 when(paymentService.pay(any())).thenReturn(PaymentResult.success()); // ... } @Test @DisplayName(\u0026#34;测试场景2\u0026#34;) void testScenario2() { // 这个测试不会受到testScenario1中Mock配置的影响 when(paymentService.pay(any())).thenReturn(PaymentResult.fail()); // ... }} 5.4 断言最佳实践 5.4.1 使用AssertJ流式断言 import static org.assertj.core.api.Assertions.*; @Test void testOrderCreation() { OrderDTO order = orderService.createOrder(userId, buildOrderRequest()); // 流式断言 - 更易读 assertThat(order) .isNotNull() .extracting(\u0026#34;orderNo\u0026#34;, \u0026#34;userId\u0026#34;, \u0026#34;status\u0026#34;) .containsExactly(\u0026#34;ORDER001\u0026#34;, userId, \u0026#34;PENDING\u0026#34;); assertThat(order.getAmount()).isPositive(); assertThat(order.getCreateTime()).isBeforeOrEqualTo(LocalDateTime.now());} 5.4.2 递归比较复杂对象 @Test void testOrderUpdate() { OrderDTO expected = buildExpectedOrder(); OrderDTO actual = orderService.updateOrder(orderId, buildUpdateRequest()); // 递归比较，忽略时间戳字段 assertThat(actual) .usingRecursiveComparison() .ignoringFields(\u0026#34;updateTime\u0026#34;, \u0026#34;version\u0026#34;) .ignoringFieldsMatchingRegexes(\u0026#34;.*Time\u0026#34;) .isEqualTo(expected);} 5.4.3 清晰的错误消息 @Test void testOrderStatusTransition() { assertEquals( \u0026#34;COMPLETED\u0026#34;, order.getStatus(), String.format(\u0026#34;订单%s在支付后状态应为COMPLETED，实际为%s\u0026#34;, order.getOrderNo(), order.getStatus()) );} 5.5 性能优化 5.5.1 共享Spring容器 // ✅ 推荐: 多个测试类使用相同配置 @SpringBootTest(classes = TestApplication.class) @ActiveProfiles(\u0026#34;test\u0026#34;) class UserServiceTest extends BaseIntegrationTest { } @SpringBootTest(classes = TestApplication.class) @ActiveProfiles(\u0026#34;test\u0026#34;) class OrderServiceTest extends BaseIntegrationTest { } // Spring会复用容器，避免重复启动 5.5.2 并行执行测试 pom.xml配置:\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;parallel\u0026gt;classes\u0026lt;/parallel\u0026gt; \u0026lt;threadCount\u0026gt;4\u0026lt;/threadCount\u0026gt; \u0026lt;forkCount\u0026gt;1\u0026lt;/forkCount\u0026gt; \u0026lt;reuseForks\u0026gt;true\u0026lt;/reuseForks\u0026gt; \u0026lt;/configuration\u0026gt;\u0026lt;/plugin\u0026gt; 注意事项:\n确保测试之间相互独立 使用@Transactional + @Rollback保证数据隔离 避免共享可变状态 5.5.3 按需初始化资源 // 使用注解标记需要的资源 @RequireRedis(false) // 不启动Redis @RequireDataSource({\u0026#34;primary\u0026#34;}) // 只初始化主数据源 class SimpleServiceTest extends BaseIntegrationTest { // 轻量级测试 } 5.6 测试覆盖率目标 推荐指标:\n行覆盖率: ≥ 80% 分支覆盖率: ≥ 70% 核心业务模块: ≥ 90% 重点关注的模块:\n✅ Service层业务逻辑 ✅ 核心算法和计算逻辑 ✅ 数据转换和校验逻辑 ✅ 异常处理逻辑 可以忽略的代码:\nPOJO的Getter/Setter 配置类 简单的委托方法 八、故障排查指南 8.1 故障排查流程图 测试失败 │ ├─ Spring容器启动失败？ │ ├─ 是 → 检查TestCallServiceApplication配置 │ │ ├─ 检查exclude配置是否正确 │ │ ├─ 检查scanBasePackageClasses是否包含必要的配置类 │ │ └─ 检查是否有循环依赖 │ │ │ └─ 否 → 继续下一步 │ ├─ 数据库初始化失败？ │ ├─ 是 → 检查SQL脚本和数据源配置 │ │ ├─ 检查schema文件路径是否正确 │ │ ├─ 检查SQL语法是否兼容H2 │ │ ├─ 检查FORCE INDEX等MySQL特有语法 │ │ └─ 检查dataSourceMap是否正确注入 │ │ │ └─ 否 → 继续下一步 │ ├─ Redis连接失败？ │ ├─ 是 → 检查内嵌Redis配置 │ │ ├─ 检查端口6380是否被占用 │ │ ├─ 检查密码是否匹配（\u0026lt;password\u0026gt;） │ │ └─ 检查Redis是否正常启动（日志） │ │ │ └─ 否 → 继续下一步 │ ├─ Dubbo Mock未生效？ │ ├─ 是 → 检查TestMockConfig配置 │ │ ├─ 检查TestMockConfig是否被扫描 │ │ ├─ 检查@Reference注解是否存在 │ │ ├─ 检查getMock()是否在测试前调用 │ │ └─ 检查Mock行为是否正确配置 │ │ │ └─ 否 → 继续下一步 │ ├─ 静态Mock未生效？ │ ├─ 是 → 检查MockedStatic配置 │ │ ├─ 检查是否在@BeforeAll中创建MockedStatic │ │ ├─ 检查是否在@AfterAll中close() │ │ └─ 检查mockito-inline依赖是否存在 │ │ │ └─ 否 → 继续下一步 │ ├─ 事务回滚未生效？ │ ├─ 是 → 检查事务配置 │ │ ├─ 检查是否添加@Transactional注解 │ │ ├─ 检查方法内是否开启了新事务 │ │ └─ 检查是否误用@Commit注解 │ │ │ └─ 否 → 继续下一步 │ └─ 其他错误 └─ 查看详细错误堆栈 ├─ 检查业务逻辑错误 ├─ 检查断言是否正确 └─ 增加日志定位问题 8.2 常见问题及解决方案 8.2.1 Spring容器启动失败 问题表现:\nError creating bean with name \u0026#39;xxxService\u0026#39;: Unsatisfied dependency... 排查步骤:\n检查 TestCallServiceApplication 的 exclude 配置 确认测试配置类被正确扫描 检查是否有循环依赖 解决方案:\n// ✅ 正确的配置 @SpringBootApplication( scanBasePackageClasses = { TestContextConfig.class, // 确保测试配置被扫描 }, exclude = { DataSourceAutoConfiguration.class, DubboAutoConfiguration.class, DubboRelaxedBinding2AutoConfiguration.class }) public class TestApplication { } 8.2.2 H2数据库SQL执行失败 问题表现:\nSyntax error in SQL statement \u0026#34;SELECT * FROM users FORCE INDEX(idx_name)[*]\u0026#34; 原因: H2不支持MySQL的 FORCE INDEX 语法\n解决方案:\n方法1: 使用SQL兼容拦截器（推荐）\n@Intercepts({ @Signature( type = StatementHandler.class, method = \u0026#34;prepare\u0026#34;, args = {Connection.class, Integer.class} )}) public class SqlCompatibilityInterceptor implements Interceptor { private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile(\u0026#34;FORCE\\\\s+INDEX\\\\s*\\\\([^)]*\\\\)\u0026#34;, Pattern.CASE_INSENSITIVE); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); String originalSql = boundSql.getSql(); // 移除FORCE INDEX String compatibleSql = FORCE_INDEX_PATTERN.matcher(originalSql).replaceAll(\u0026#34;\u0026#34;); // 更新SQL Field sqlField = boundSql.getClass().getDeclaredField(\u0026#34;sql\u0026#34;); sqlField.setAccessible(true); sqlField.set(boundSql, compatibleSql); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); }} 方法2: 修改SQL文件，移除 FORCE INDEX\n8.2.3 内嵌Redis启动失败 问题表现:\nredis.embedded.exceptions.RedisBuildingException: Could not start Redis server java.net.BindException: Address already in use 原因: 6380端口被占用\n解决方案:\n方法1: 杀掉占用端口的进程\n# 查找占用端口的进程 lsof -i:6380 # 杀掉进程 kill -9 \u0026lt;PID\u0026gt; 方法2: 修改Redis端口\n// BaseIntegrationTest.java redisServer = new RedisServerBuilder() .bind(\u0026#34;localhost\u0026#34;) .port(6381) // 改为其他端口 .setting(\u0026#34;requirepass \u0026lt;password\u0026gt;\u0026#34;) .build(); # application-testcase.properties redis.port=6381 # 对应修改配置 8.2.4 Dubbo Mock未生效 问题表现:\nNullPointerException: Cannot invoke method on null object 排查清单:\n确认 TestMockConfig 被Spring扫描 确认字段上有 @Reference 注解 确认在测试方法中调用了 getMock() 并配置了行为 调试代码:\n@Test void debugDubboMock() { // 1. 验证Mock对象是否存在 UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); assertNotNull(mock, \u0026#34;Mock对象应该被自动注入\u0026#34;); // 2. 配置Mock行为 UserDTO mockUser = buildMockUser(); when(mock.getUserById(anyLong())).thenReturn(mockUser); // 3. 执行业务方法 UserDTO result = orderService.getUserInfo(1001L); // 4. 验证Mock被调用 verify(mock, times(1)).getUserById(1001L); // 5. 验证返回结果 assertEquals(mockUser.getId(), result.getId());} 常见错误:\n// ❌ 错误：忘记配置Mock行为 UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); // 直接调用业务方法，Mock返回null orderService.createOrder(...); // NPE! // ✅ 正确：先配置Mock行为 UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); when(mock.getUserById(anyLong())).thenReturn(buildMockUser()); orderService.createOrder(...); // OK 8.2.5 MockedStatic未正确关闭导致测试干扰 问题表现:\n第一个测试类通过，第二个测试类失败 org.mockito.exceptions.base.MockitoException: For com.aliyun.openservices.ons.api.ONSFactory, static mocking is already registered 原因: MockedStatic 对象未在 @AfterAll 中关闭\n解决方案:\n@SpringBootTest public abstract class BaseIntegrationTest { private static MockedStatic\u0026lt;ONSFactory\u0026gt; mockedOnsFactory; @BeforeAll public static void initBeforeClass() { mockedOnsFactory = Mockito.mockStatic(ONSFactory.class); // ... } @AfterAll public static void afterAll() { // ⚠️ 必须关闭，否则影响其他测试类 if (mockedOnsFactory != null) { mockedOnsFactory.close(); } }} 8.2.6 多数据源初始化重复执行 问题表现:\n数据库初始化日志打印多次 测试数据重复插入导致主键冲突 原因: Spring容器复用时，InitializingBean.afterPropertiesSet() 被多次调用\n解决方案:\n@Slf4j public abstract class BaseCommonServiceTest extends BaseIntegrationTest implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { doInitDatasource(); } void doInitDatasource() { // ✅ 使用单例标志位，确保只初始化一次 if (TestDataSourceConfig.isDatasourceInitialized()) { log.info(\u0026#34;数据源已初始化，跳过\u0026#34;); return; } log.info(\u0026#34;开始初始化测试数据库...\u0026#34;); // 初始化逻辑... TestDataSourceConfig.setDatasourceInitialized(); }} 8.2.7 测试运行速度慢 优化策略:\n复用Spring容器 // ✅ 所有测试类使用相同的配置 @SpringBootTest(classes = TestCallServiceApplication.class) @ActiveProfiles(\u0026#34;testcase\u0026#34;) public abstract class BaseIntegrationTest { } // Spring会自动复用容器，避免重复启动 减少数据库连接池大小 # application-testcase.properties spring.datasource.druid.initial-size=1 spring.datasource.druid.min-idle=1 spring.datasource.druid.max-active=5 # 测试环境无需太大 使用并行执行 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;parallel\u0026gt;classes\u0026lt;/parallel\u0026gt; \u0026lt;threadCount\u0026gt;4\u0026lt;/threadCount\u0026gt; \u0026lt;forkCount\u0026gt;1\u0026lt;/forkCount\u0026gt; \u0026lt;reuseForks\u0026gt;true\u0026lt;/reuseForks\u0026gt; \u0026lt;/configuration\u0026gt;\u0026lt;/plugin\u0026gt; 按需初始化数据源 // 如果测试只用到部分数据源，可以跳过其他数据源初始化 void doInitDatasource() { if (TestDataSourceConfig.isDatasourceInitialized()) { return; } // 只初始化需要的数据源 initDatabase(\u0026#34;dataSourceMaster\u0026#34;, \u0026#34;schema/master-schema.sql\u0026#34;, \u0026#34;data/master-data.sql\u0026#34;); // 其他数据源按需初始化 TestDataSourceConfig.setDatasourceInitialized(); } 九、从现有测试迁移 9.1 迁移策略 原则: 渐进式迁移，保留原有单元测试，逐步增加集成测试\n阶段1: 搭建框架（1-2天） └─ 创建测试基类和配置 阶段2: 迁移核心流程（1周） └─ 选择3-5个核心业务流程编写集成测试 阶段3: 补充边界场景（持续） └─ 根据优先级逐步覆盖其他场景 阶段4: 质量门禁（1天） └─ 集成到CI流程，设置覆盖率阈值 9.2 从单元测试迁移 场景: 现有大量单元测试使用Mock，希望转为集成测试\n迁移前（单元测试）:\n@ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private OrderMapper orderMapper; @Mock private OrderLineMapper orderLineMapper; @Mock private UserRemoteService userRemoteService; @Mock private RedisTemplate\u0026lt;String, Object\u0026gt; redisTemplate; @InjectMocks private OrderServiceImpl orderService; @Test void testCreateOrder() { // 需要Mock所有依赖 when(orderMapper.insert(any())).thenReturn(1); when(userRemoteService.getUserById(anyLong())).thenReturn(buildMockUser()); when(redisTemplate.opsForValue()).thenReturn(mock(ValueOperations.class)); // ... 更多Mock配置 OrderDTO result = orderService.create(buildRequest()); assertNotNull(result); verify(orderMapper, times(1)).insert(any()); }} 迁移后（集成测试）:\n@SpringBootTest class OrderServiceIntegrationTest extends BaseCommonServiceTest { @Autowired private OrderService orderService; @Test @DisplayName(\u0026#34;创建订单-完整流程测试\u0026#34;) void testCreateOrder() { // 只需Mock外部Dubbo服务 UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); when(userService.getUserById(anyLong())).thenReturn(buildMockUser()); // Mapper、Redis等都真实执行 OrderDTO result = orderService.create(buildRequest()); // 验证结果 assertNotNull(result.getId()); assertEquals(\u0026#34;PENDING\u0026#34;, result.getStatus()); // 验证数据库真实写入 OrderDTO dbRecord = orderService.getById(result.getId()); assertNotNull(dbRecord); assertEquals(result.getOrderName(), dbRecord.getOrderName()); }} 对比:\n维度 单元测试 集成测试 Mock数量 需要Mock所有依赖（5+） 只Mock外部服务（1-2个） 代码量 50-100行 20-30行 真实性 低（所有依赖都是假的） 高（数据库、Redis真实） 维护成本 高（依赖变更需调整Mock） 低（只关注业务逻辑） 执行速度 快（毫秒级） 较快（秒级，首次启动慢） 9.3 迁移检查清单 迁移前准备:\n了解项目的技术栈（Dubbo版本、数据源数量） 确认JDK版本（项目使用JDK 1.8） 确认Spring Boot版本（建议使用2.x或3.x） 评估需要Mock的外部服务（Dubbo、OSS、MQ等） 搭建框架:\n创建 TestApplication 启动类 创建 BaseIntegrationTest 基类 创建 BaseCommonServiceTest 服务测试基类 配置 TestDataSourceConfig 多数据源 配置 TestMockConfig Dubbo自动Mock 准备 application-testcase.properties 配置文件 准备各数据源的 schema.sql 和 data.sql 编写首个测试:\n选择一个简单的Service编写集成测试 验证Spring容器启动成功 验证数据库初始化成功 验证Redis连接成功 验证Dubbo Mock生效 持续完善:\n为核心业务流程编写集成测试 设置JaCoCo覆盖率目标（80%） 集成到Maven构建流程 编写测试文档和示例 十、常见问题 10.1 H2与MySQL的SQL差异 10.1.1 H2不支持FORCE INDEX 解决方案: 使用MyBatis拦截器移除\n@Intercepts({@Signature( type = StatementHandler.class, method = \u0026#34;prepare\u0026#34;, args = {Connection.class, Integer.class})}) public class ForceIndexInterceptor implements Interceptor { private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile(\u0026#34;FORCE\\\\s+INDEX\\\\s*\\\\([^)]*\\\\)\u0026#34;, Pattern.CASE_INSENSITIVE); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); String sql = handler.getBoundSql().getSql(); String compatibleSql = FORCE_INDEX_PATTERN.matcher(sql).replaceAll(\u0026#34;\u0026#34;); // 更新SQL并继续执行 return invocation.proceed(); }} 10.1.2 日期函数差异 -- MySQL SELECT DATE_FORMAT(create_time, \u0026#39;%Y-%m-%d\u0026#39;) FROM orders; -- H2兼容写法 SELECT FORMATDATETIME(create_time, \u0026#39;yyyy-MM-dd\u0026#39;) FROM orders; 解决方案:\n使用H2兼容模式: jdbc:h2:mem:testdb;MODE=MySQL 创建自定义函数映射 使用TestContainers运行真实MySQL 10.2 事务回滚不生效 10.2.1 症状 测试数据未被清理，影响其他测试\n10.2.2 原因 缺少@Transactional注解 方法内部开启了新事务 使用了@Commit而非@Rollback 解决方案 // ✅ 正确写法 @Transactional @Rollback @Test void testCreate() { service.create(entity); // 测试结束自动回滚 } // ❌ 错误写法 @Test void testCreate() { service.create(entity); // 不会回滚！ } 10.3 Mock未生效 10.3.1 排查清单 确认TestMockConfig已被Spring扫描 确认字段上有@Reference或@FeignClient注解 确认Mock对象已注册到MockRegistry 在测试方法中验证Mock配置 10.3.2 调试代码 @Test void debugMock() { ExternalService mock = TestMockConfig.getMock(ExternalService.class); assertNotNull(mock, \u0026#34;Mock对象应该存在\u0026#34;); // 配置行为 when(mock.call()).thenReturn(\u0026#34;test\u0026#34;); // 验证调用 service.doSomething(); verify(mock, atLeastOnce()).call();} 10.4 并发测试不稳定 10.4.1 问题 并发测试偶尔失败，结果不可预测\n解决方案 使用CountDownLatch同步多线程\n@Test void testConcurrentOperation() throws InterruptedException { int threadCount = 10; CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch endLatch = new CountDownLatch(threadCount); ExecutorService executor = Executors.newFixedThreadPool(threadCount); List\u0026lt;Future\u0026lt;Result\u0026gt;\u0026gt; futures = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; threadCount; i++) { Future\u0026lt;Result\u0026gt; future = executor.submit(() -\u0026gt; { startLatch.await(); // 等待统一开始 Result result = service.process(); endLatch.countDown(); return result; }); futures.add(future); } startLatch.countDown(); // 开始 endLatch.await(10, TimeUnit.SECONDS); // 等待完成 // 验证结果 assertEquals(threadCount, futures.stream() .filter(f -\u0026gt; { try { return f.get() != null; } catch (Exception e) { return false; } }) .count());} 10.5 测试运行速度慢 10.5.1 优化策略 使用H2而非真实数据库 # H2 (快) spring.datasource.url=jdbc:h2:mem:testdb # MySQL (慢) spring.datasource.url=jdbc:mysql://localhost:3306/test 减少不必要的组件扫描 @SpringBootTest(classes = TestApplication.class) // 只扫描必要的包 并行执行测试 \u0026lt;parallel\u0026gt;classes\u0026lt;/parallel\u0026gt; \u0026lt;threadCount\u0026gt;4\u0026lt;/threadCount\u0026gt; 复用Spring容器 使用相同的@SpringBootTest配置 使用相同的@ActiveProfiles 十一、附录：实施案例 11.1 项目背景 典型项目概况:\n业务领域: 典型的企业级业务系统（如订单管理、任务调度等） 技术栈: Spring Boot 2.1.x (企业定制版本) JDK 1.8 MyBatis 3.5.7 Dubbo（RPC框架） Dynamic DataSource 3.6.0（多数据源） Redis（缓存） RocketMQ（消息队列） Apollo（配置中心） 架构复杂度: 7个数据源: master、slave-one、slave-two、slave-three、drds-master、cs-crm、warehouse 多个Dubbo远程服务: 用户服务、计费服务、CRM服务等 外部依赖: 阿里云OSS、RocketMQ、短信服务 11.2 实施前的挑战 测试覆盖率低: 仅30%，大量核心业务逻辑未覆盖 测试编写困难: 需要手动Mock 10+个依赖，每个测试50-100行 测试不稳定: 依赖外部环境（MySQL、Redis、Dubbo注册中心） 执行速度慢: 需要启动完整服务，测试执行5分钟+ 维护成本高: 依赖变更需要修改大量Mock代码 11.3 实施方案 11.3.1 框架搭建（2天） 第一步: 创建测试基类体系\nBaseIntegrationTest (基础设施) └─ BaseCommonServiceTest (数据初始化) └─ 具体业务测试类 第二步: 配置7个H2内存数据源\n// TestDataSourceConfig.java @Bean(name = \u0026#34;dataSourceMaster\u0026#34;) @Bean(name = \u0026#34;dataSourceSlaveOne\u0026#34;) @Bean(name = \u0026#34;dataSourceSlaveTwo\u0026#34;) @Bean(name = \u0026#34;dataSourceSlaveThree\u0026#34;) @Bean(name = \u0026#34;dataSourceDrdsMaster\u0026#34;) @Bean(name = \u0026#34;dataSourceCsCrm\u0026#34;) @Bean(name = \u0026#34;dataSourceWareHouse\u0026#34;) 第三步: 实现Dubbo自动Mock\n// TestMockConfig.java @Component public class TestMockConfig implements BeanPostProcessor { // 自动扫描@Reference注解，注入Mock对象 } 第四步: Mock静态工厂类\n// BaseIntegrationTest.java MockedStatic\u0026lt;ONSFactory\u0026gt; mockedOnsFactory; // RocketMQ MockedStatic\u0026lt;OssClientFactory\u0026gt; mockedOssClientFactory; // OSS 11.3.2 SQL兼容性处理 问题: 项目中大量SQL使用了 FORCE INDEX，H2不支持\n解决方案: 实现MyBatis拦截器\n@Intercepts({@Signature(...)}) public class SqlCompatibilityInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 移除FORCE INDEX语法 String sql = boundSql.getSql(); String compatibleSql = FORCE_INDEX_PATTERN.matcher(sql).replaceAll(\u0026#34;\u0026#34;); // ... }} 11.3.3 核心流程测试编写（1周） 选择5个核心业务流程编写集成测试：\n订单创建流程: 创建订单 → 保存数据库 → 缓存 → 发送MQ 数据导入流程: 解析文件 → 批量插入 → 数据校验 状态变更流程: 状态机流转 → 触发事件 → 更新统计 业务审批流程: 多级审批 → 状态更新 → 通知 统计查询: 多数据源聚合 → Redis缓存 示例测试:\n@SpringBootTest public class OrderServiceIntegrationTest extends BaseCommonServiceTest { @Autowired private OrderService orderService; @Test @DisplayName(\u0026#34;订单创建-完整流程测试\u0026#34;) void testCreateOrder() { // 1. Mock外部Dubbo服务 UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); when(userService.getUserById(anyLong())).thenReturn(buildMockUser()); // 2. 执行业务操作 OrderCreateRequest request = buildOrderRequest(); OrderDTO result = orderService.create(request); // 3. 验证数据库 assertNotNull(result.getId()); OrderDTO dbRecord = orderService.getById(result.getId()); assertEquals(request.getOrderNo(), dbRecord.getOrderNo()); // 4. 验证Redis缓存 String cacheKey = \u0026#34;order:\u0026#34; + result.getId(); assertTrue(redisTemplate.hasKey(cacheKey)); // 5. 验证Dubbo服务调用 verify(userService, times(1)).getUserById(anyLong()); } @Test @DisplayName(\u0026#34;数据导入-批量导入1000条数据\u0026#34;) void testImportData() { // 准备测试数据 OrderDTO order = createOrder(); List\u0026lt;OrderLineDTO\u0026gt; lines = buildOrderLines(1000); // 执行导入 ImportResult result = orderService.importLines(order.getId(), lines); // 验证结果 assertEquals(1000, result.getSuccessCount()); assertEquals(0, result.getFailCount()); // 验证数据库 List\u0026lt;OrderLineDTO\u0026gt; dbLines = orderLineService.listByOrderId(order.getId()); assertEquals(1000, dbLines.size()); }} 11.4 实施效果 11.4.1 量化指标 指标 实施前 实施后 提升 测试覆盖率 30% 85% +183% 测试执行时间 5分钟 45秒 -85% 测试用例编写时间 2小时/个 30分钟/个 -75% Mock代码行数 50-100行 5-10行 -90% 测试稳定性 70%（依赖外部环境） 99%（独立环境） +41% 发现Bug数量 - 12个潜在问题 - 11.4.2 质量提升 发现的典型问题:\n空指针异常: 3处未检查null的代码 并发问题: 2处缓存更新竞态条件 数据一致性: 4处跨数据源事务未正确处理 性能问题: 1处N+1查询 边界条件: 2处未处理空列表的情况 11.4.3 开发效率提升 重构信心增强:\n有完整的集成测试保护，重构时能快速发现问题 代码评审时能运行测试验证改动 新功能开发加速:\n新增功能先写测试，明确需求和边界 TDD方式开发，减少返工 问题定位快速:\n线上问题能通过集成测试快速复现 修复后运行测试验证 11.5 经验总结 11.5.1 成功关键因素 自动化Mock: TestMockConfig自动处理Dubbo服务Mock，减少90%的Mock代码 多数据源方案: 7个H2数据库完全模拟生产环境 SQL兼容拦截器: 解决FORCE INDEX等MySQL特有语法 Builder模式: 简化测试数据构建 单例初始化: 防止多次初始化数据库，提升速度 11.5.2 避坑指南 MockedStatic必须关闭: 忘记在@AfterAll中close()会导致其他测试失败 事务回滚: 忘记添加@Transactional导致测试数据残留 端口冲突: Redis 6380端口被占用导致启动失败 Spring容器复用: 不同配置会导致容器重启，降低速度 H2兼容性: 部分MySQL语法需要适配 11.5.3 推广建议 适合推广的项目特征:\n✅ 使用Spring Boot 2.x ✅ 使用Dubbo或Feign作为RPC框架 ✅ 有多数据源 ✅ 依赖外部服务（MQ、OSS等） ✅ 业务逻辑复杂，需要全链路测试 不适合的场景:\n❌ 简单的CRUD应用（过度设计） ❌ 对SQL兼容性要求极高（建议用TestContainers） ❌ 测试执行时间要求\u0026lt;5秒（首次启动慢） 十二、总结与展望 12.1 核心价值 本框架通过以下设计实现高质量、高效率的集成测试：\n环境隔离:\nH2内存数据库，无需外部MySQL 内嵌Redis，无需外部Redis Mock Dubbo服务，无需注册中心 独立测试环境，不干扰生产 快速执行:\n首次启动15-30秒，后续测试秒级 Spring容器复用，避免重复启动 并行执行，充分利用CPU 自动Mock:\nDubbo服务自动Mock，减少90%代码 静态工厂类集中Mock 动态配置Mock行为 易于维护:\nBuilder模式构建测试数据 清晰的三层架构（基础设施/配置/业务） 单元测试和集成测试互补 质量保障:\nJaCoCo覆盖率监控 全链路业务流程验证 发现潜在Bug 12.2 适用范围 推荐使用的项目:\nSpring Boot 2.x / 3.x 微服务项目 使用Dubbo/Feign的分布式系统 多数据源业务场景 依赖大量外部服务的应用 需要快速反馈的敏捷团队 强调质量的企业级项目 可简化的场景:\n简单的CRUD应用（可能过度设计） 对数据库SQL兼容性要求极高（建议使用TestContainers） 测试执行时间要求\u0026lt;5秒的场景 12.3 版本兼容性 组件 当前项目版本 兼容版本范围 说明 JDK 1.8 1.8 - 17 JDK 17+需调整反射相关代码 Spring Boot 2.1.x 2.x - 3.x 3.x需改为jakarta包 MyBatis 3.5.7 3.5.x - Dubbo 2.7.x 2.6.x - 3.x 3.x注解包名变更 Dynamic DataSource 3.6.0 3.x - H2 Database 2.x 2.x MySQL兼容模式 Mockito 4.x 4.x - 5.x 需mockito-inline支持静态Mock Embedded Redis 0.7.3 0.7.x Windows需特殊配置 12.4 持续改进方向 契约测试:\n使用Pact验证Dubbo服务契约 确保服务间接口一致性 混合策略:\n常规测试使用H2 关键流程使用TestContainers真实MySQL 性能测试使用生产环境数据 测试数据工厂:\n统一的测试数据生成器 快速生成各种场景数据 支持数据变体生成 可视化报告:\n集成Allure生成美观的测试报告 测试执行趋势分析 覆盖率变化监控 智能化测试:\n基于代码变更自动生成测试用例 AI辅助编写断言 自动识别需要Mock的依赖 12.5 参考资源 Spring Boot Testing Documentation Mockito Documentation AssertJ Guide JaCoCo Documentation H2 Database Documentation Dynamic DataSource TestContainers (可选，真实数据库测试) 十三、快速参考 13.1 常用命令 # 运行所有测试 mvn clean test # 运行指定测试类 mvn test -Dtest=OrderServiceIntegrationTest # 运行指定测试方法 mvn test -Dtest=OrderServiceIntegrationTest#testCreateOrder # 生成覆盖率报告 mvn clean test jacoco:report # 查看覆盖率报告 open target/site/jacoco/index.html # 跳过测试 mvn clean install -DskipTests # 并行执行测试（4线程） mvn test -T 4 13.2 关键配置速查 H2数据源:\nspring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= 内嵌Redis:\nredisServer = new RedisServerBuilder() .bind(\u0026#34;localhost\u0026#34;) .port(6380) .setting(\u0026#34;requirepass \u0026lt;password\u0026gt;\u0026#34;) .build(); Dubbo Mock:\nUserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); when(mock.getUserById(anyLong())).thenReturn(buildMockUser()); 静态工厂Mock:\nMockedStatic\u0026lt;ONSFactory\u0026gt; mockedFactory = Mockito.mockStatic(ONSFactory.class); mockedFactory.when(() -\u0026gt; ONSFactory.createProducer(any())).thenReturn(mockProducer); // 必须在@AfterAll中close() 13.3 测试模板 @SpringBootTest public class XxxServiceIntegrationTest extends BaseCommonServiceTest { @Autowired private XxxService xxxService; private YyyRemoteService yyyRemoteService; @BeforeEach void setUp() { // 获取并重置Mock yyyRemoteService = TestMockConfig.getMock(YyyRemoteService.class); Mockito.reset(yyyRemoteService); // 配置通用Mock行为 when(yyyRemoteService.query(anyLong())).thenReturn(buildDefaultData()); } @Test @DisplayName(\u0026#34;测试业务场景-描述预期结果\u0026#34;) void testBusinessScenario() { // 1. 准备数据 XxxDTO input = buildXxxDTO(); // 2. 配置特定Mock行为（可选） when(yyyRemoteService.query(1001L)).thenReturn(buildSpecificData()); // 3. 执行业务操作 XxxDTO result = xxxService.process(input); // 4. 验证结果 assertNotNull(result); assertEquals(expectedValue, result.getValue()); // 5. 验证数据库（可选） XxxDTO dbRecord = xxxService.getById(result.getId()); assertNotNull(dbRecord); // 6. 验证Mock调用（可选） verify(yyyRemoteService, times(1)).query(anyLong()); }} 文档版本: v4.0 (通用版) 最后更新: 2026-06-08 适用项目: Spring Boot + Dubbo + 多数据源的企业级项目 维护者: 测试框架团队\n","permalink":"https://blog1.awalks.com/posts/spring-boot-integration-testing-guide/","summary":"一套通用、高效、易维护的 Spring Boot 集成测试框架：H2 多数据源模拟、Dubbo 自动 Mock、SQL 兼容拦截器与覆盖率保障。","title":"Spring Boot 集成测试框架搭建指南"},{"content":"全新项目启动与交付基线（通用版） 1. 文档定位 本文适用于以下场景：\n从零建设一个新产品或新系统； 将原型、脚本、人工流程或验证版升级为正式系统； 多个前后端应用、异步任务、外部数据源共同参与的复杂项目； 对正确性、可追溯性、性能或发布稳定性有明确要求的项目； 包含规则引擎、算法、AI、大模型或其他非确定性执行单元的项目。 本文不是一套要求“先写完所有文档再编码”的瀑布流程。它只要求在正式开发前，冻结那些一旦变更就会引发跨模块、跨应用或跨数据层返工的高耦合决策；其余细节应通过最小垂直切片快速验证和迭代。\n本文中的约束级别：\n必须：不满足时不得进入下一阶段； 应当：默认执行，确有理由不执行时需要记录决策； 可选：根据项目规模、风险和成本选择。 2. 核心原则 2.1 少写说明文档，多建立可执行合同 架构图和说明文档只能帮助理解，不能保证实现一致。关键约束应尽量转化为可以被工具验证的合同：\n产品验收矩阵； 领域状态迁移测试； OpenAPI、RPC IDL 或消息 Schema； DDL、数据字典和 Schema Version； 错误码和失败分类； Golden Dataset 或确定性验收样本； 性能预算和容量阈值； CI、集成测试和发布门禁。 2.2 先冻结高耦合决策，再实现最小闭环 编码前不需要确定所有页面细节和内部实现，但必须明确：\n系统解决什么问题，不解决什么问题； 每个核心概念的含义和不变量； 应用与模块的职责边界； 接口、事件、状态和数据的权威合同； 正确结果如何判断； 生产容量、延迟、成本和可用性约束； 第一条端到端链路如何验收。 随后只实现一条最小但真实的垂直链路，即 Walking Skeleton，再逐步扩展覆盖面。\n2.3 业务结果与技术执行状态必须分离 系统必须区分：\n业务判断结果，例如通过、不通过、不可判定； 技术执行状态，例如成功、失败、跳过、超时； 规则适用性，例如适用、不适用、条件不足； 流程状态，例如待执行、运行中、已完成、部分失败、已取消。 禁止用业务上的“无问题”掩盖技术执行失败，也禁止因为技术执行完成就直接认定业务处理成功。\n2.4 默认选择少量部署单元和清晰模块边界 全新项目不等于必须拆分大量微服务。默认建议采用模块化单体或少量部署组，通过模块、端口和依赖规则保证边界。\n只有满足以下条件之一时，才考虑拆出新的独立部署单元：\n需要独立扩缩容； 存在明确的安全或网络边界； 由不同团队独立发布和维护； 资源模型明显不同； 故障隔离收益能够覆盖部署复杂度。 2.5 并行化实现不能并行化事实源 适合并行的工作：\n只读审计； 独立模块实现； 测试补充； 文档和数据核对； 不共享合同的页面或适配器。 不适合无协调并行的工作：\nDDL 和领域模型； 公共 API 和消息协议； 状态机； 核心算法或规则合同； 公共组件和跨应用基础类； 初始化数据和版本迁移。 这些内容必须指定唯一负责人、变更顺序和集成验证人。\n3. 最小项目基线包 一个新项目在正式开发前，应至少产出以下七类内容。内容可以很短，但必须明确、版本化并有负责人。\ndocs/ 00-project-charter.md # 项目目标、范围、非目标、成功指标 01-domain-contract.md # 领域词典、不变量、状态机 02-system-context.md # 系统边界、职责矩阵、核心时序 03-data-and-capacity.md # 数据分类、ERD、容量与访问模式 04-acceptance-and-test-plan.md # 验收矩阵、样本、测试层级和门禁 05-release-runbook.md # 环境、发布、回滚和故障处理 adr/ # 只记录高影响、难逆转的决策 contracts/ api/ # OpenAPI、RPC IDL events/ # MQ/Event Schema schemas/ # 数据、配置或算法输入输出 Schema 项目较小时可以合并文件，但不能省略对应决策。\n4. Phase 0：项目启动与范围冻结 4.1 项目章程 项目章程必须回答：\n问题 要求 解决什么问题 用一段话说明业务痛点和目标用户 成功是什么 给出可验证的业务指标和工程指标 V1 包含什么 列出必须交付的核心场景 V1 不包含什么 明确延期能力，防止范围自然膨胀 谁负责决策 明确产品、技术、质量和发布负责人 最大风险是什么 列出前三项高不确定性，并给出验证方式 4.2 场景优先级 所有功能按以下方式分层：\nP0 主闭环：没有它，系统没有业务价值； P1 完整性：主闭环可用后补齐； P2 平台化：配置化、运营化、自动化扩展； 明确不做：当前版本不进入设计和实现。 如果 V1 同时包含主业务闭环、运营配置平台、复杂任务调度、动态规则、完整管理后台和多种外部集成，应重新评估它是否仍然属于 MVP。\n4.3 权威事实源矩阵 同一类事实只能有一个权威源。其他材料只能引用它，不能复制后独立演进。\n事实类别 建议权威源 负责人 变更方式 自动门禁 页面与交互 产品原型和验收矩阵 产品负责人 原型版本 浏览器 E2E、视觉验收 领域语义 领域合同和状态机 领域负责人 ADR、合同版本 状态迁移测试 API OpenAPI 或 RPC IDL 接口负责人 兼容版本 消费者合同测试 消息 Event Schema 消息负责人 Schema Version Producer/Consumer 测试 数据 DDL 和数据字典 数据负责人 Migration Schema 校验 算法或规则 版本化执行包和 Manifest 算法负责人 Package Version Golden Diff 验收结论 Golden Dataset 和验收报告 质量负责人 样本版本 CI/发布门禁 4.4 Phase 0 退出条件 P0 场景和非目标已经确认； 成功指标可以被度量； 每类事实有且只有一个权威源； 所有 P0 歧义已经解决，或明确安排验证实验； 产品、技术、质量和发布责任人已经确定。 5. Phase 1：领域、边界与合同冻结 5.1 领域词典 每个核心名词至少定义：\n唯一名称和业务含义； 唯一标识； 生命周期； 与其他实体的数量关系； 创建、修改和终止条件； 是否需要版本化； 是否属于业务事实、执行状态或派生结果。 应重点消除以下歧义：\n同一概念多个名称； 同一名称多个含义； 业务状态和执行状态混用； 配置、版本和运行快照混用； 错误、异常、问题、缺陷等概念边界不清。 5.2 不变量 不变量是无论经过哪条流程都必须成立的业务约束，例如：\n发布后的版本不可原地修改； 同一幂等键只能产生一个业务结果； 每个执行单元最终必须进入唯一终态； 技术失败不能被记录为业务成功； 派生结果必须能够追溯到输入版本和执行版本； 任何状态迁移都必须记录操作者、时间和原因。 不变量应进入测试，而不是只写在说明文档中。\n5.3 状态机 每个长流程至少定义：\n初始状态； 合法迁移； 终态； 超时和租约； 重试与最大次数； 取消和补偿； 并发写入规则； 部分失败语义； 人工恢复入口。 建议将状态拆成三层：\n流程状态：PENDING / RUNNING / SUCCEEDED / PARTIAL_FAILED / FAILED / CANCELLED 执行状态：SUCCESS / ERROR / TIMEOUT / SKIPPED 业务结果：由具体领域定义，例如 PASS / FAIL / NOT_JUDGABLE 5.4 系统边界和职责矩阵 对每个应用或模块记录：\n应用或模块 拥有的数据 提供的能力 允许依赖 禁止承担 接入层/BFF 登录态、会话上下文 鉴权、校验、聚合 内部业务 API 核心业务规则 控制面 配置、版本、任务控制 创建、发布、调度、查询 数据面、存储 重计算执行细节 数据面/Worker 执行状态、运行结果 拉取、处理、计算、写结果 外部 Provider 面向终端的通用接口 外部适配器 不拥有业务事实 协议转换、限流、重试 外部系统 编排业务状态 通用推荐架构：\nflowchart LR Client[\u0026#34;客户端\u0026#34;] --\u0026gt; Access[\u0026#34;接入层 / BFF\u0026#34;] Access --\u0026gt; Control[\u0026#34;业务控制面\u0026#34;] Control --\u0026gt; Store[\u0026#34;事务数据存储\u0026#34;] Control --\u0026gt; Outbox[\u0026#34;Outbox / 消息队列\u0026#34;] Outbox --\u0026gt; Worker[\u0026#34;Worker / 数据面\u0026#34;] Worker --\u0026gt; Provider[\u0026#34;外部数据源或服务\u0026#34;] Worker --\u0026gt; Engine[\u0026#34;规则、算法或处理引擎\u0026#34;] Worker --\u0026gt; Result[\u0026#34;结果与审计存储\u0026#34;] Result --\u0026gt; Access 该图表达职责关系，不代表必须拆成多个微服务。\n5.5 API、事件和错误合同 接口合同至少包含：\n请求和响应字段语义； 必填性、默认值和枚举； 幂等键； 鉴权上下文和可信字段来源； 超时和重试责任方； 分页方式； 错误码、是否可重试和用户提示； 版本兼容期和废弃流程。 长任务应优先采用异步协议：\n提交请求 -\u0026gt; 返回 operation_id/task_id -\u0026gt; 查询或订阅进度 -\u0026gt; 获取最终结果 禁止让同步 HTTP 超时承担长任务完成语义。\n大数据列表应默认采用“有界过滤条件 + cursor”，谨慎使用总数查询和 offset 深分页。\n5.6 Phase 1 退出条件 核心领域词典和不变量已确认； 所有长流程有状态机； 应用和模块职责无重叠或空白； P0 API、事件和错误码已经形成机器可读合同； 幂等、超时、重试、取消和部分失败语义已经明确； 关键合同存在自动化校验。 6. 数据模型与容量基线 6.1 数据分类 所有数据先分类，再决定存储：\n数据类型 示例 设计重点 配置与版本 规则、方案、模板 版本化、发布后不可变 任务控制 任务、分片、租约 状态机、幂等、并发控制 执行明细 单项处理记录 数据量、分区、批量写 派生结果 评分、分析结果 可重算性、覆盖策略 原始事实 外部日志、原始事件 来源指针、保留期、合规性 快照 执行时输入集合 可重放、版本和去重 审计记录 操作、状态迁移 不可篡改、可追溯 6.2 每张表的准入问题 新增表或大字段前必须回答：\n谁写入？ 谁读取？ 读取场景和查询条件是什么？ 为什么不能由现有权威数据计算得到？ 数据是否重复，哪一份是权威源？ 保留多久，如何清理？ 日增量和三年规模是多少？ 唯一键、分区键和主要索引是什么？ 是否需要审计、回放或重算？ 删除它会损失什么能力？ 如果无法回答读取方和保留策略，默认不新增。\n6.3 容量模型 编码前至少估算：\n日新增量 峰值 QPS / TPS 单条平均与最大大小 在线保留天数 主查询时间范围 单任务最大处理量 单次批量大小 外部服务限流 计算、存储和第三方调用成本 容量约束必须进入接口和数据模型。例如底层按时间分区时，上层任务和分页协议也必须携带时间范围，不能等到性能优化阶段再补。\n6.4 原始数据、快照和派生结果 通用建议：\n原始大对象优先保存来源指针、哈希和必要元数据； 需要重放时保存一份权威快照，不在多张表和多个 JSON 字段重复复制； 派生结果记录输入版本、配置版本和执行器版本； 明确哪些结果可以覆盖，哪些记录只能追加； 清理策略、归档策略和恢复能力必须成对设计。 6.5 数据门禁 DDL 可以在目标数据库版本上全量初始化； Migration 可以从上一版本升级且可验证； 数据字典与 DDL 一致； 所有大表有容量、分区和清理策略； 所有唯一键和幂等键存在数据库级约束； 没有无法说明读取方的表和大字段； 核心查询有 Explain 或等价执行计划验证。 7. 非功能需求基线 非功能需求不是上线前的“优化项”。会影响接口、状态机和数据结构的约束必须前置。\n7.1 性能与成本 至少定义：\n关键接口 p95、p99； 单任务最大耗时； 单处理单元最大耗时； 目标吞吐； 最大并发； 外部调用次数和成本预算； 扩容后吞吐是否应线性提升； 降级时允许损失什么，不允许损失什么。 7.2 可靠性 至少定义：\n可用性目标； RPO、RTO； 幂等策略； 重试分类； 死信或失败队列； 租约和失联恢复； 对账任务； 部分失败和人工恢复。 7.3 可观测性 核心链路应统一携带：\ntrace_id request_id / operation_id task_id / item_id entity_id provider_request_id executor_version 最低指标集：\n队列等待时间； 各阶段 p95、p99； 成功、失败、超时、跳过数量； 重试和死信数量； 外部 Provider 错误率； 数据完整率； 单任务资源和第三方成本； 长时间未推进任务数量； 大数据源扫描量和返回量。 日志必须能够回答“在哪一阶段、处理哪个对象、使用哪个版本、因为什么失败”，不能只打印最终异常。\n7.4 安全与权限 在不阻塞首个功能闭环的前提下，以下基础约束必须在联调和发布前完成：\n用户身份只来自可信接入层； 服务间使用独立身份； 权限按动作设计，不只按页面设计； 密钥通过 Secret、密钥系统或受控配置管理； 日志、测试报告和会话不得输出密钥、Cookie 和完整敏感数据； 数据库 DDL、读、写账号分权； 审计记录包含操作者、动作、对象、时间和结果。 8. Phase 2：单条真实链路 Walking Skeleton 8.1 目标 Walking Skeleton 不是 Mock 演示，而是一条真实、可追踪、可重复执行的最小业务闭环：\n真实输入 -\u0026gt; 真实接入接口 -\u0026gt; 真实业务状态机 -\u0026gt; 真实存储 -\u0026gt; 至少一个真实外部依赖或代表性替身 -\u0026gt; 真实执行 -\u0026gt; 可查询结果 -\u0026gt; 用户可见输出 8.2 必须覆盖 一条正常路径； 一条业务不满足路径； 一条技术失败路径； 重复提交； 超时或重试； 状态恢复； 全链路 Trace； 数据可回查； 结果可由验收样本判断。 8.3 退出条件 重复执行结果幂等； 没有永久停留在中间状态的对象； 技术错误不会被记为业务成功； 每一步都有结构化日志和指标； 输入、配置、执行器和输出版本可追溯； 一键执行测试可以重复得到相同结论； 失败后存在自动或人工恢复方式。 Walking Skeleton 通过前，不应同时铺开全部页面、规则、Provider 和后台配置能力。\n9. Phase 3：代表集、全链路与性能验收 9.1 分层样本 建议建立三层样本：\n层级 用途 特点 单条样本 开发期快速验证 典型、证据完整、结论明确 代表集 日常回归 覆盖正常、边界、异常和各类分支 发布集 发布前验收 覆盖真实分布、历史缺陷和长尾场景 样本必须版本化，记录来源、输入快照、预期结果、允许误差和变更原因。\n9.2 测试层级 静态检查和模块边界 -\u0026gt; 单元测试 -\u0026gt; 状态机与合同测试 -\u0026gt; 数据库和外部适配器集成测试 -\u0026gt; 单条 Walking Skeleton -\u0026gt; 代表集端到端测试 -\u0026gt; 浏览器产品验收 -\u0026gt; 性能、容量和故障注入 -\u0026gt; 发布集回归 9.3 全链路成功的定义 禁止只用 HTTP 200、任务完成或进程退出码判断成功。全链路验收至少校验：\n流程终态正确； 所有适用执行单元都有唯一结果； 没有隐藏的 Provider、脚本或异步消费错误； 必填上下文完整； 数据库行数、唯一性和状态守恒正确； 业务结果与预期一致； 错误分类和 reason_code 正确； 耗时、吞吐和成本在预算内； 日志、指标和审计完整。 9.4 测试环境要求 测试数据库必须可重复初始化； 每次执行使用干净数据集或唯一运行隔离标识； 测试依赖缺失必须失败，禁止静默跳过核心合同； Mock 只用于单元测试，发布验收使用真实依赖或经过证明的高保真替身； 测试脚本与运行期脚本必须分目录管理； 测试报告应机器可读，并能阻断 CI 或发布。 10. Phase 4：发布与运行门禁 10.1 发布前检查 P0 产品验收全部通过； API 和消息消费者合同测试通过； DDL/Migration 已在目标数据库验证； 代表集和发布集通过； 无隐藏失败和异常中间状态； 性能、容量和成本满足预算； 配置差异已审查； 告警、看板和对账任务已就绪； 灰度和回滚方案已演练； 三端或多应用版本兼容矩阵已确认； 发布负责人和观察窗口已明确。 10.2 多仓库发布清单 跨仓库项目应使用同一份发布 Manifest：\nrelease_id: example-v1 contracts_version: v1 database_schema_version: 202601010001 applications: - name: frontend branch: feature/example-v1 commit: \u0026lt;commit\u0026gt; - name: backend branch: feature/example-v1 commit: \u0026lt;commit\u0026gt; - name: worker branch: feature/example-v1 commit: \u0026lt;commit\u0026gt; acceptance_report: \u0026lt;artifact\u0026gt; rollback_runbook: \u0026lt;artifact\u0026gt; 示例只表达结构，真实 Manifest 不应存放密钥。\n10.3 发布后的观察 发布后必须关注：\n新版本请求量和错误率； 队列积压和任务停滞； 数据库扫描量、慢查询和容量增长； 外部 Provider 错误率； 业务结果分布是否突变； 新旧版本结果差异； 人工反馈和回滚触发条件。 11. 变更治理 11.1 冻结不等于禁止变更 需求和架构可以变化，但必须按影响范围更新权威源：\n提出变更 -\u0026gt; 识别受影响的领域、API、数据、算法和验收样本 -\u0026gt; 记录 ADR 或变更说明 -\u0026gt; 先修改合同和兼容策略 -\u0026gt; 修改实现 -\u0026gt; 执行回归 -\u0026gt; 发布新版本 禁止先修改实现，再通过补文档解释现状。\n11.2 必须记录 ADR 的决策 应用拆分或合并； 数据存储位置和分区策略； 权威算法或规则执行源； 长任务协议和消息语义； 状态机重大变化； 兼容性破坏； 外部 Provider 的替换； 影响性能、成本或安全边界的决策。 11.3 ADR 最小模板 # ADR-NNN：决策标题 ## 状态 Proposed / Accepted / Superseded ## 背景 为什么需要决策，当前约束是什么。 ## 决策 选择什么方案。 ## 备选方案 考虑过哪些方案，为什么没有选择。 ## 影响 对 API、数据、运行、成本、测试和迁移的影响。 ## 验证与回滚 如何证明决策有效，失败时如何撤销。 12. 并行开发与智能体协作规范 12.1 任务拆分 每个并行任务必须明确：\n目标和完成标准； 所有文件或模块； 允许修改的合同； 禁止修改的共享区域； 依赖的版本和前置任务； 验证命令； 集成负责人。 12.2 合同所有权 同一时间，下列内容只能有一个修改负责人：\n领域核心模型； 公共接口； 数据库 Schema； 消息协议； 状态机； 核心执行算法； 公共基础设施类。 其他执行者可以审查和提出建议，但不能同时提交相互竞争的实现。\n12.3 每轮并行后的统一验收 并行任务完成后必须进行一次统一集成：\n检查合同是否漂移； 合并和解决冲突； 执行完整构建； 执行合同和状态机测试； 执行代表集回归； 更新进度、决策和风险； 关闭已完成的执行单元。 13. 常见反模式 13.1 文档很多，但没有准入门槛 表现：方案反复评审，编码后仍持续讨论接口、DDL 和边界。\n改进：为每个阶段定义退出条件，未通过不得扩大实现范围。\n13.2 把验证版直接当正式系统设计 表现：Mock、临时字段、简化状态和单机假设进入正式实现。\n改进：明确验证版只证明哪些假设；正式实现只继承验证结论，不继承临时代码和数据模型。\n13.3 流程跑完就算成功 表现：任务成功，但内部调用失败、数据为空或结果不可判定。\n改进：验收必须同时校验流程状态、技术执行、业务结果和数据守恒。\n13.4 先存所有数据，后续再清理 表现：相同原始内容在事件表、快照和 JSON 字段中重复保存。\n改进：先确定权威原始数据、重放需求和保留期，只保存一份必要快照。\n13.5 把生产环境当集成测试环境 表现：接口、DDL、依赖和核心逻辑在部署后才首次组合验证。\n改进：在发布前构建与生产拓扑接近的集成环境，并执行自动化门禁。\n13.6 性能问题全部后置 表现：大表分区键、分页协议和外部限流到上线前才纳入设计。\n改进：优化可以后置，容量约束和访问模式不能后置。\n13.7 通过新增兜底掩盖合同错误 表现：输入缺失、状态异常或协议错误时不断增加默认值和旧逻辑兼容。\n改进：全新项目应修正权威合同和数据源，禁止保留相互矛盾的语义。\n13.8 依赖聊天记录交接项目状态 表现：新任务需要复制大量历史结论，事实和完成状态难以确认。\n改进：维护项目章程、ADR、合同版本、验收报告和发布 Manifest，聊天只用于推动工作，不作为长期事实源。\n14. 项目启动总检查表 14.1 编码前 项目目标、P0 范围和非目标明确； 成功指标可度量； 权威事实源矩阵完成； 领域词典和不变量完成； 核心状态机完成； 系统边界和职责矩阵完成； P0 API、事件和错误合同完成； 数据分类、ERD 和容量模型完成； 非功能预算完成； 单条验收样本和预期结果完成； 环境、配置和密钥管理方式明确； 关键 ADR 已接受。 14.2 扩大开发范围前 单条 Walking Skeleton 通过； 正常、业务不满足和技术失败路径均已验证； 状态无悬挂； 重复执行幂等； 全链路可观测； 数据可回查； 合同和 Schema 自动测试通过。 14.3 联调前 代表集已经建立； 数据库可重复初始化； 外部依赖和限流策略明确； 三端消费者合同测试通过； 测试脚本和运行期脚本分离； 核心依赖缺失不会被静默跳过； 错误码、日志和指标可以定位具体阶段。 14.4 发布前 产品验收矩阵通过； 发布集或 Golden Dataset 通过； 性能、容量和成本预算通过； 无隐藏失败和异常终态； 数据迁移和兼容性验证通过； 灰度、回滚和对账方案通过； 版本 Manifest 完整； 监控、告警和观察窗口就绪； 敏感凭据未进入代码、日志、报告和聊天交接材料。 15. 最终判断标准 一个项目是否真正具备进入下一阶段的条件，不看已经写了多少代码或文档，而看以下问题是否能明确回答：\n我们正在交付的最小业务闭环是什么？ 谁定义产品、领域、接口、数据和算法的最终事实？ 任意失败发生时，系统会进入什么状态，如何恢复？ 任意结果能否追溯到输入、配置和执行版本？ 真实数据量和真实依赖下，系统是否仍然成立？ 自动化测试能否识别“流程成功但内部失败”的假绿？ 多应用、多仓库和并行开发是否共享同一套合同？ 发布失败时是否可以快速发现、停止和回滚？ 如果这些问题不能被合同、测试或运行证据回答，项目就还没有形成可靠的交付基线。\n","permalink":"https://blog1.awalks.com/posts/project-delivery-baseline/","summary":"从零建设新产品/系统的通用交付基线：冻结高耦合决策、可执行合同、Walking Skeleton 与发布门禁。","title":"全新项目启动与交付基线（通用版）"}]