Spring Boot 集成测试框架搭建指南 链接到标题
🎯 目标: 提供一套通用、高效、易维护的集成测试框架,适用于任何 Spring Boot 项目
📚 目录 链接到标题
一、快速开始 链接到标题
1.1 环境要求 链接到标题
在开始前,请确认以下环境:
| 组件 | 版本要求 | 当前项目版本 | 说明 |
|---|---|---|---|
| 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分钟上手 链接到标题
第一步: 添加依赖(项目已包含)
<!-- Spring Boot 测试 --><dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency>
<!-- H2 内存数据库 --><dependency>
<groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope></dependency>
<!-- Mockito Inline (支持静态Mock) -->
<dependency>
<groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <scope>test</scope></dependency>
<!-- Embedded Redis -->
<dependency>
<groupId>it.ozimov</groupId> <artifactId>embedded-redis</artifactId> <version>0.7.3</version> <scope>test</scope></dependency>
第二步: 创建测试基类
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("testcase")
public abstract class BaseIntegrationTest {
// 所有集成测试继承此类
}
第三步: 编写测试
public class YourServiceTest extends BaseCommonServiceTest {
@Autowired private YourService yourService; @Test
@DisplayName("测试核心业务流程")
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 适用场景 链接到标题
✅ 适合使用的场景:
- 微服务架构的复杂系统
- 多数据源业务场景
- 依赖大量外部服务的应用
- 需要快速反馈的敏捷团队
- 强调质量的企业级项目
⚠️ 可以简化的场景:
- 简单的CRUD应用(可能过度设计)
- 对数据库SQL兼容性要求极高(建议使用 TestContainers)
- 测试执行时间要求<5秒的场景
二、必需文件清单 链接到标题
2.1 完整目录结构 链接到标题
基于 Call-Service 项目的实际实现,以下是完整的测试框架目录结构:
src/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 {
}
要点:
- 排除生产环境的自动配置(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("testcase")
@SpringBootTest(classes = TestApplication.class)
public abstract class BaseIntegrationTest {
protected static RedisServer redisServer; private static MockedStatic<ONSFactory> mockedOnsFactory; private static MockedStatic<OssClientFactory> mockedOssClientFactory;
@BeforeAll public static void initBeforeClass() { log.info("集成测试环境初始化开始...");
// Mock RocketMQ Producer
mockRocketMQ(); // Mock OSS Client
mockOssClient(); // 启动内嵌Redis
initRedis(); // 初始化Apollo配置
initApolloConfig(); log.info("集成测试环境初始化完成");
} /**
* 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(() -> 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(() -> OssClientFactory.getOSSClient()).thenReturn(mockOssClient); } /**
* 启动内嵌Redis服务
*/ protected static void initRedis() { redisServer = new RedisServerBuilder() .bind("localhost") .port(6380) .setting("requirepass <password>") .build(); redisServer.start(); log.info("内嵌Redis启动成功,端口: 6380");
} /**
* 初始化Apollo配置(本地模式)
*/ protected static void initApolloConfig() { System.setProperty("app.id", "your-app-name"); System.setProperty("apollo.cacheDir", "src/test/resources"); System.setProperty("apollo.meta", "http://none"); System.setProperty("env", "local"); System.setProperty("apollo.bootstrap.enabled", "true"); log.info("Apollo配置初始化完成");
}
@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 && redisServer.isActive()) { redisServer.stop(); log.info("内嵌Redis已停止");
} }}
要点:
- 使用
@BeforeAll初始化共享资源(Redis、静态Mock) - 使用
MockedStaticMock静态工厂类(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<String, DataSource> dataSourceMap;
@Override public void afterPropertiesSet() throws Exception { doInitDatasource(); }
/** * 初始化所有数据源的Schema和测试数据
* 使用单例标志位,确保只初始化一次
*/ void doInitDatasource() { if (TestDataSourceConfig.isDatasourceInitialized()) { return; } log.info("开始初始化测试数据库...");
// 初始化各个数据源
initDatabase("dataSourceMaster", "schema/master-schema.sql", "data/master-data.sql"); initDatabase("dataSourceSlaveOne", "schema/slaveone-schema.sql", ""); initDatabase("dataSourceSlaveTwo", "schema/master-schema.sql", "data/master-data.sql"); initDatabase("dataSourceSlaveThree", "schema/slavethree-schema.sql", ""); initDatabase("dataSourceDrdsMaster", "schema/drdsmaster-schema.sql", "data/drdsmaster-data.sql"); initDatabase("dataSourceCsCrm", "schema/cscrm-schema.sql", ""); initDatabase("dataSourceWareHouse", "schema/datawarehouse-schema.sql", "data/datawarehouse-data.sql"); TestDataSourceConfig.setDatasourceInitialized();
log.info("测试数据库初始化完成");
}
/** * 初始化指定数据源的表结构和数据
* @param dataSourceKey 数据源标识
* @param schemaLocation schema SQL文件路径
* @param dataLocation 数据SQL文件路径(可为空)
*/ protected void initDatabase(String dataSourceKey, String schemaLocation, String dataLocation) { if (dataSourceMap == null) { log.warn("dataSourceMap为空,无法初始化数据库");
return; }
DataSource dataSource = dataSourceMap.get(dataSourceKey); if (dataSource == null) { throw new IllegalArgumentException("未找到数据源: " + dataSourceKey);
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
if (schemaLocation != null && !schemaLocation.isEmpty()) { populator.addScript(new ClassPathResource(schemaLocation)); }
if (dataLocation != null && !dataLocation.isEmpty()) { populator.addScript(new ClassPathResource(dataLocation)); }
try { log.info("初始化数据源 [{}]", dataSourceKey); populator.execute(dataSource); } catch (Exception e) { throw new RuntimeException("初始化数据库失败: " + dataSourceKey, e);
} }}
要点:
- 继承自
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=<your-password>
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参数说明:
MODE=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%) \ 单个类/方法
/__________________\
实践建议:
- 单元测试: 覆盖所有公共方法、边界条件、异常处理
- 集成测试: 覆盖核心业务流程、多组件协作场景
- E2E测试: 仅覆盖最关键的用户路径
2.2.2 测试隔离原则 链接到标题
环境隔离:
// 独立的测试启动类
@SpringBootApplication(
scanBasePackageClasses = {TestConfig.class}, exclude = {DataSourceAutoConfiguration.class})
public class TestApplication {}
数据隔离:
// 事务自动回滚
@Transactional
@Rollback
@Test
void testCreate() {
service.create(entity); // 测试结束自动回滚
}
Mock隔离:
// 每个测试独立配置Mock
@BeforeEach
void setUp() {
ExternalService mock = getMock(ExternalService.class); when(mock.call()).thenReturn(testData);}
四、核心组件 链接到标题
4.1 BaseIntegrationTest - 测试基类 链接到标题
职责: 提供测试运行的基础环境和生命周期管理
核心功能:
- Spring Boot 测试环境启动
- 内嵌服务管理(Redis、MQ等)
- 静态服务Mock(工厂类、配置类)
- 资源生命周期管理
实现模板:
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("test")
public abstract class BaseIntegrationTest {
protected static EmbeddedRedis redisServer;
protected static MockedStatic<StaticFactory> 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
核心配置项:
@Configuration
@ComponentScan(
basePackages = "com.yourcompany.yourapp", excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX, pattern = ".*ProductionConfig"
))
@MapperScan("com.yourcompany.yourapp.mapper")
public class TestConfig {
// Redis配置
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6380); } // RestTemplate配置
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(); } // MyBatis拦截器(兼容H2)
@Bean
public SqlCompatibilityInterceptor sqlInterceptor() {
return new SqlCompatibilityInterceptor(); }}
4.3 TestDataSourceConfig - 多数据源配置 链接到标题
职责: 配置多个H2内存数据库,模拟生产环境的多数据源架构
Call-Service项目的多数据源架构:
- master: 主库(读写)
- slave-one: 从库1
- slave-two: 从库2(master的只读副本)
- slave-three: 从库3
- drds-master: DRDS主库
- cs-crm: CS-CRM数据源
- warehouse: 数据仓库
完整配置实现:
package 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 = "dataSourceMaster") @Primary @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSource dataSourceMaster() { return new DruidDataSource(); }
/** * 从数据源1
*/ @Bean(name = "dataSourceSlaveOne") @ConfigurationProperties(prefix = "spring.datasource.druid.slave-one") public DataSource dataSourceSlaveOne() { return new DruidDataSource(); }
/** * 从数据源2(master的只读副本)
*/ @Bean(name = "dataSourceSlaveTwo") @ConfigurationProperties(prefix = "spring.datasource.druid.slave-two") public DataSource dataSourceSlaveTwo() { return new DruidDataSource(); }
/** * 从数据源3
*/ @Bean(name = "dataSourceSlaveThree") @ConfigurationProperties(prefix = "spring.datasource.druid.slave-three") public DataSource dataSourceSlaveThree() { return new DruidDataSource(); }
/** * DRDS主数据源
*/ @Bean(name = "dataSourceDrdsMaster") @ConfigurationProperties(prefix = "spring.datasource.druid.drds-master") public DataSource dataSourceDrdsMaster() { return new DruidDataSource(); }
/** * CS-CRM数据源
*/ @Bean(name = "dataSourceCsCrm") @ConfigurationProperties(prefix = "spring.datasource.druid.cs-crm") public DataSource dataSourceCsCrm() { return new DruidDataSource(); }
/** * 数据仓库数据源
*/ @Bean(name = "dataSourceWareHouse") @ConfigurationProperties(prefix = "spring.datasource.druid.warehouse") public DataSource dataSourceWareHouse() { return new DruidDataSource(); }
/** * 注入所有数据源到Map,方便BaseCommonServiceTest批量初始化
*/ @Bean public Map<String, DataSource> dataSourceMap( DataSource dataSourceMaster, DataSource dataSourceSlaveOne, DataSource dataSourceSlaveTwo, DataSource dataSourceSlaveThree, DataSource dataSourceDrdsMaster, DataSource dataSourceCsCrm, DataSource dataSourceWareHouse ) { Map<String, DataSource> map = new HashMap<>(); map.put("dataSourceMaster", dataSourceMaster); map.put("dataSourceSlaveOne", dataSourceSlaveOne); map.put("dataSourceSlaveTwo", dataSourceSlaveTwo); map.put("dataSourceSlaveThree", dataSourceSlaveThree); map.put("dataSourceDrdsMaster", dataSourceDrdsMaster); map.put("dataSourceCsCrm", dataSourceCsCrm); map.put("dataSourceWareHouse", dataSourceWareHouse); return map; }}
配置要点:
- 使用Druid连接池: 与生产环境保持一致
- @Primary注解: 标记主数据源,避免注入歧义
- 单例标志位: 防止多次初始化(Spring容器复用时)
- dataSourceMap: 方便测试基类批量初始化所有数据源
对应的配置文件:
# 主数据源
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注解)
实现原理:
使用Spring的 BeanPostProcessor 在Bean初始化时扫描字段,发现 @Reference 注解后注入Mock对象
完整实现:
package 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<Class<?>, Object> mockRegistry = new ConcurrentHashMap<>();
@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<?> serviceInterface = field.getType(); // 从注册表获取或创建Mock对象
Object mockObject = mockRegistry.computeIfAbsent( serviceInterface, clazz -> Mockito.mock(clazz)
); // 注入Mock对象到字段
field.setAccessible(true); field.set(bean, mockObject); } catch (Exception e) {
throw new RuntimeException("Mock Dubbo服务失败: " + field.getName(), e);
} }
/** * 获取Mock对象,用于测试中配置Mock行为
** @param clazz 服务接口Class
* @return Mock对象
*/ public static <T> T getMock(Class<T> clazz) { Object mock = mockRegistry.get(clazz); if (mock == null) { throw new IllegalStateException("Mock对象未注册: " + clazz.getName());
} return (T) mock; } /**
* 清空Mock注册表(仅用于特殊场景)
*/ public static void clearMockRegistry() { mockRegistry.clear(); }}
使用示例:
@SpringBootTest
public class OrderServiceIntegrationTest extends BaseCommonServiceTest {
@Autowired
private OrderService orderService; // 被测试的服务
// OrderService内部依赖的Dubbo服务会被自动Mock
@Test
@DisplayName("测试订单创建(依赖外部用户服务)")
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("PENDING", order.getStatus()); // 5. 验证Dubbo服务被调用
verify(userService, times(1)).getUserById(anyLong()); }}
优势:
- ✅ 自动化: 无需手动Mock每个Dubbo服务
- ✅ 集中管理: 所有Mock对象在注册表中统一管理
- ✅ 易于配置: 通过
getMock()获取Mock对象后即可配置行为 - ✅ 单例模式: 同一接口的Mock对象在所有Bean中共享
注意事项:
- Mock对象在整个测试类生命周期中共享,需要在
@BeforeEach中重置Mock行为 - 如果需要不同测试使用不同Mock行为,需要在每个测试方法中重新配置
4.5 SqlCompatibilityInterceptor - SQL兼容拦截器 链接到标题
职责: 兼容MySQL特有语法,使其能在H2上运行
常见兼容场景:
- 移除
FORCE INDEX(...)提示 - 转换日期函数
- 处理分页语法差异
@Intercepts({
@Signature(
type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )})
public class SqlCompatibilityInterceptor implements Interceptor {
private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile("FORCE\\s+INDEX\\s*\\([^)]*\\)", 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("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, compatibleSql); return invocation.proceed();
} private String makeCompatible(String sql) {
// 移除FORCE INDEX
sql = FORCE_INDEX_PATTERN.matcher(sql).replaceAll("");
// 其他兼容处理...
return sql; } @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); }}
五、搭建步骤 链接到标题
步骤 1: 添加Maven依赖 链接到标题
<dependencies>
<!-- Spring Boot 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- H2 内存数据库 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> <!-- Mockito (支持静态Mock) -->
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> <!-- AssertJ (流式断言) -->
<dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <!-- 内嵌Redis (可选) -->
<dependency> <groupId>it.ozimov</groupId> <artifactId>embedded-redis</artifactId> <version>0.7.3</version> <scope>test</scope> </dependency> <!-- TestContainers (可选,用于真实数据库) -->
<dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <version>1.19.0</version> <scope>test</scope> </dependency></dependencies>
步骤 2: 配置测试环境 链接到标题
创建 application-test.properties:
# 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=<your-password>
# 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
-- 用户表
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
-- 插入测试用户
INSERT INTO users (username, email, status) VALUES ('testuser1', 'test1@example.com', 'ACTIVE'),
('testuser2', 'test2@example.com', 'ACTIVE');
-- 插入测试订单
INSERT INTO orders (user_id, order_no, amount, status) VALUES (1, 'ORDER001', 100.00, 'PENDING'),
(1, 'ORDER002', 200.00, 'COMPLETED');
步骤 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("test")
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("requirepass <password>") .build(); redisServer.start(); } catch (Exception e) { throw new RuntimeException("Redis启动失败", 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("测试用户创建流程")
void testCreateUser() { // 准备测试数据
UserCreateRequest request = UserCreateRequest.builder() .username("newuser") .email("new@example.com") .build(); // 执行业务操作
UserDTO result = userService.createUser(request); // 验证结果
assertNotNull(result.getId()); assertEquals("newuser", result.getUsername()); assertEquals("ACTIVE", result.getStatus()); } @Test
@DisplayName("测试重复用户名抛出异常")
void testDuplicateUsername() { UserCreateRequest request = UserCreateRequest.builder() .username("testuser1") // 已存在
.email("duplicate@example.com") .build(); assertThrows(DuplicateUserException.class, () -> {
userService.createUser(request); }); }}
步骤 7: 配置覆盖率检查 链接到标题
添加 JaCoCo 插件:
<plugin>
<groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> <execution> <id>check</id> <goals> <goal>check</goal> </goals> <configuration> <rules> <rule> <element>BUNDLE</element> <limits> <limit> <counter>LINE</counter> <value>COVEREDRATIO</value> <minimum>0.80</minimum> </limit> </limits> </rule> </rules> </configuration> </execution> </executions></plugin>
运行测试并查看覆盖率:
# 执行测试
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(业务测试优先)
@Test
@DisplayName("创建订单-成功场景")
void testCreateOrder() {
// ...}
@Test
@DisplayName("创建订单-订单号重复时抛出异常")
void testCreateOrderWithDuplicateOrderNo() {
// ...}
@Test
@DisplayName("导入数据-批量导入1000条数据")
void testImportData() {
// ...}
备选方式: should_When模式(技术组件测试)
@Test
@DisplayName("should_ReturnUser_When_ValidIdProvided")
void shouldReturnUserWhenValidIdProvided() {
// ...}
@Test
@DisplayName("should_ThrowException_When_UserNotFound")
void shouldThrowExceptionWhenUserNotFound() {
// ...}
测试方法命名原则:
- 方法名使用驼峰命名,以
test开头 - 使用
@DisplayName提供中文业务描述 - 一个测试方法只测试一个场景
- 测试名称要体现测试意图和预期结果
6.3 测试数据命名 链接到标题
// ✅ 推荐: build + 实体名
private OrderDTO buildOrder() {
return OrderDTO.builder() .orderName("测试订单_" + 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 全链路集成测试 链接到标题
目的: 模拟真实业务流程,验证多个组件协作
@Test
@DisplayName("订单全链路集成测试")
@Transactional
@Rollback
void testOrderFullFlow() {
// 1. 创建用户
UserDTO user = userService.createUser(buildUserRequest()); // 2. 创建订单
OrderDTO order = orderService.createOrder(user.getId(), buildOrderRequest()); assertNotNull(order.getOrderNo()); assertEquals("PENDING", order.getStatus()); // 3. 支付订单
PaymentResult payment = orderService.pay(order.getId(), buildPaymentRequest()); assertTrue(payment.isSuccess()); // 4. 验证订单状态
OrderDTO paidOrder = orderService.getById(order.getId()); assertEquals("PAID", paidOrder.getStatus()); // 5. 发货
orderService.ship(order.getId()); // 6. 完成订单
OrderDTO completedOrder = orderService.getById(order.getId()); assertEquals("COMPLETED", completedOrder.getStatus());}
7.1.2 参数化测试 链接到标题
目的: 用一个测试方法覆盖多种场景
@ParameterizedTest
@DisplayName("测试各种无效邮箱格式")
@ValueSource(strings = {
"invalid", "invalid@", "@example.com", "invalid@.com", ""})
void testInvalidEmail(String invalidEmail) {
UserCreateRequest request = UserCreateRequest.builder() .username("testuser") .email(invalidEmail) .build(); assertThrows(ValidationException.class, () -> {
userService.createUser(request);
});}
@ParameterizedTest
@DisplayName("测试不同订单金额的手续费计算")
@CsvSource({
"100.00, 1.00", // 金额, 预期手续费
"1000.00, 10.00", "10000.00, 100.00"})
void testOrderFeeCalculation(BigDecimal amount, BigDecimal expectedFee) {
OrderDTO order = orderService.createOrder(userId, amount); assertEquals(expectedFee, order.getFee());}
7.1.3 异常场景测试 链接到标题
目的: 验证系统的异常处理能力
@Test
@DisplayName("测试外部服务失败时的降级处理")
void testExternalServiceFailure() {
// Mock外部服务抛出异常
PaymentService paymentMock = TestMockConfig.getMock(PaymentService.class); when(paymentMock.pay(any())) .thenThrow(new RemoteServiceException("支付服务不可用"));
// 执行业务操作
OrderDTO order = orderService.createOrder(userId, buildOrderRequest()); PaymentResult result = orderService.pay(order.getId(), buildPaymentRequest()); // 验证降级处理
assertFalse(result.isSuccess()); assertEquals("PAYMENT_SERVICE_ERROR", result.getErrorCode()); // 验证订单状态未变化
OrderDTO unchangedOrder = orderService.getById(order.getId()); assertEquals("PENDING", 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 = "testuser"; private String email = "test@example.com"; private String status = "ACTIVE"; 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 + "_" + System.currentTimeMillis()) .email(email) .status(status) .build(); } }}
// 使用示例
@Test
void testCreateUser() {
UserCreateRequest request = TestDataBuilder.userRequest() .username("customuser") .email("custom@example.com") .build(); UserDTO result = userService.createUser(request);
assertNotNull(result);}
7.2.2 使用@Sql初始化数据 链接到标题
@Test
@DisplayName("测试查询大量订单的性能")
@Sql(scripts = "/test-data/large-orders.sql")
void testQueryLargeOrders() {
List<OrderDTO> orders = orderService.queryByUserId(userId); assertTrue(orders.size() >= 1000);}
@Test
@Sql(scripts = {
"/test-data/setup-users.sql", "/test-data/setup-orders.sql"})
@Sql(scripts = "/test-data/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testWithSpecificData() {
// 测试逻辑
}
7.3 Dubbo Mock使用原则 链接到标题
7.3.1 什么应该Mock 链接到标题
✅ 应该Mock:
- Dubbo远程服务(@Reference注解的服务)
- 外部HTTP接口
- 第三方服务(OSS、短信、邮件)
- RocketMQ Producer
- 难以构造的对象
- 不稳定的依赖
❌ 不应该Mock:
- 被测试的核心业务逻辑
- 简单的POJO对象
- 数据访问层(Mapper/DAO)- 用内存数据库真实测试
- Spring容器管理的Bean(Service、Component)
Call-Service项目中需要Mock的组件:
// 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("测试调用用户服务-不同响应场景")
void testCallUserService() {
// 获取自动Mock的Dubbo服务
UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); // 场景1: 正常返回用户
UserDTO mockUser = UserDTO.builder() .id(1001L) .username("testuser") .build(); when(userService.getUserById(1001L)).thenReturn(mockUser); // 场景2: 用户不存在返回null
when(userService.getUserById(9999L)).thenReturn(null); // 场景3: 服务异常抛出RpcException
when(userService.getUserById(0L)) .thenThrow(new RpcException("服务不可用"));
// 执行测试
UserDTO result1 = orderService.getUser(1001L); assertNotNull(result1); UserDTO result2 = orderService.getUser(9999L);
assertNull(result2); assertThrows(RpcException.class, () -> {
orderService.getUser(0L); });}
7.3.3 验证Dubbo服务调用 链接到标题
@Test
@DisplayName("测试创建订单时调用用户服务验证用户")
void testCreateOrderCallsUserService() {
UserRemoteService userService = TestMockConfig.getMock(UserRemoteService.class); // 配置Mock返回
when(userService.getUserById(anyLong())).thenReturn(buildMockUser()); // 执行业务操作
OrderDTO order = orderService.createOrder(1001L, buildOrderRequest()); // 验证Dubbo服务被正确调用
ArgumentCaptor<Long> 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("测试场景1")
void testScenario1() {
// 特定场景的Mock配置
when(paymentService.pay(any())).thenReturn(PaymentResult.success());
// ... }
@Test
@DisplayName("测试场景2")
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("orderNo", "userId", "status") .containsExactly("ORDER001", userId, "PENDING"); 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("updateTime", "version") .ignoringFieldsMatchingRegexes(".*Time") .isEqualTo(expected);}
5.4.3 清晰的错误消息 链接到标题
@Test
void testOrderStatusTransition() {
assertEquals( "COMPLETED", order.getStatus(),
String.format("订单%s在支付后状态应为COMPLETED,实际为%s",
order.getOrderNo(), order.getStatus())
);}
5.5 性能优化 链接到标题
5.5.1 共享Spring容器 链接到标题
// ✅ 推荐: 多个测试类使用相同配置
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("test")
class UserServiceTest extends BaseIntegrationTest { }
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("test")
class OrderServiceTest extends BaseIntegrationTest { }
// Spring会复用容器,避免重复启动
5.5.2 并行执行测试 链接到标题
pom.xml配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0</version> <configuration> <parallel>classes</parallel> <threadCount>4</threadCount> <forkCount>1</forkCount> <reuseForks>true</reuseForks> </configuration></plugin>
注意事项:
- 确保测试之间相互独立
- 使用
@Transactional+@Rollback保证数据隔离 - 避免共享可变状态
5.5.3 按需初始化资源 链接到标题
// 使用注解标记需要的资源
@RequireRedis(false) // 不启动Redis
@RequireDataSource({"primary"}) // 只初始化主数据源
class SimpleServiceTest extends BaseIntegrationTest {
// 轻量级测试
}
5.6 测试覆盖率目标 链接到标题
推荐指标:
- 行覆盖率: ≥ 80%
- 分支覆盖率: ≥ 70%
- 核心业务模块: ≥ 90%
重点关注的模块:
- ✅ Service层业务逻辑
- ✅ 核心算法和计算逻辑
- ✅ 数据转换和校验逻辑
- ✅ 异常处理逻辑
可以忽略的代码:
- POJO的Getter/Setter
- 配置类
- 简单的委托方法
八、故障排查指南 链接到标题
8.1 故障排查流程图 链接到标题
测试失败
│ ├─ Spring容器启动失败?
│ ├─ 是 → 检查TestCallServiceApplication配置
│ │ ├─ 检查exclude配置是否正确
│ │ ├─ 检查scanBasePackageClasses是否包含必要的配置类
│ │ └─ 检查是否有循环依赖
│ │ │ └─ 否 → 继续下一步
│ ├─ 数据库初始化失败?
│ ├─ 是 → 检查SQL脚本和数据源配置
│ │ ├─ 检查schema文件路径是否正确
│ │ ├─ 检查SQL语法是否兼容H2
│ │ ├─ 检查FORCE INDEX等MySQL特有语法
│ │ └─ 检查dataSourceMap是否正确注入
│ │ │ └─ 否 → 继续下一步
│ ├─ Redis连接失败?
│ ├─ 是 → 检查内嵌Redis配置
│ │ ├─ 检查端口6380是否被占用
│ │ ├─ 检查密码是否匹配(<password>)
│ │ └─ 检查Redis是否正常启动(日志)
│ │ │ └─ 否 → 继续下一步
│ ├─ Dubbo Mock未生效?
│ ├─ 是 → 检查TestMockConfig配置
│ │ ├─ 检查TestMockConfig是否被扫描
│ │ ├─ 检查@Reference注解是否存在
│ │ ├─ 检查getMock()是否在测试前调用
│ │ └─ 检查Mock行为是否正确配置
│ │ │ └─ 否 → 继续下一步
│ ├─ 静态Mock未生效?
│ ├─ 是 → 检查MockedStatic配置
│ │ ├─ 检查是否在@BeforeAll中创建MockedStatic
│ │ ├─ 检查是否在@AfterAll中close()
│ │ └─ 检查mockito-inline依赖是否存在
│ │ │ └─ 否 → 继续下一步
│ ├─ 事务回滚未生效?
│ ├─ 是 → 检查事务配置
│ │ ├─ 检查是否添加@Transactional注解
│ │ ├─ 检查方法内是否开启了新事务
│ │ └─ 检查是否误用@Commit注解
│ │ │ └─ 否 → 继续下一步
│ └─ 其他错误
└─ 查看详细错误堆栈
├─ 检查业务逻辑错误
├─ 检查断言是否正确
└─ 增加日志定位问题
8.2 常见问题及解决方案 链接到标题
8.2.1 Spring容器启动失败 链接到标题
问题表现:
Error creating bean with name 'xxxService': Unsatisfied dependency...
排查步骤:
- 检查
TestCallServiceApplication的exclude配置 - 确认测试配置类被正确扫描
- 检查是否有循环依赖
解决方案:
// ✅ 正确的配置
@SpringBootApplication(
scanBasePackageClasses = { TestContextConfig.class, // 确保测试配置被扫描
}, exclude = { DataSourceAutoConfiguration.class, DubboAutoConfiguration.class, DubboRelaxedBinding2AutoConfiguration.class })
public class TestApplication {
}
8.2.2 H2数据库SQL执行失败 链接到标题
问题表现:
Syntax error in SQL statement "SELECT * FROM users FORCE INDEX(idx_name)[*]"
原因: H2不支持MySQL的 FORCE INDEX 语法
解决方案:
方法1: 使用SQL兼容拦截器(推荐)
@Intercepts({
@Signature(
type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )})
public class SqlCompatibilityInterceptor implements Interceptor {
private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile("FORCE\\s+INDEX\\s*\\([^)]*\\)", 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("");
// 更新SQL
Field sqlField = boundSql.getClass().getDeclaredField("sql"); 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
8.2.3 内嵌Redis启动失败 链接到标题
问题表现:
redis.embedded.exceptions.RedisBuildingException: Could not start Redis server
java.net.BindException: Address already in use
原因: 6380端口被占用
解决方案:
方法1: 杀掉占用端口的进程
# 查找占用端口的进程
lsof -i:6380
# 杀掉进程
kill -9 <PID>
方法2: 修改Redis端口
// BaseIntegrationTest.java
redisServer = new RedisServerBuilder()
.bind("localhost") .port(6381) // 改为其他端口
.setting("requirepass <password>") .build();
# application-testcase.properties
redis.port=6381 # 对应修改配置
8.2.4 Dubbo Mock未生效 链接到标题
问题表现:
NullPointerException: Cannot invoke method on null object
排查清单:
- 确认
TestMockConfig被Spring扫描 - 确认字段上有
@Reference注解 - 确认在测试方法中调用了
getMock()并配置了行为
调试代码:
@Test
void debugDubboMock() {
// 1. 验证Mock对象是否存在
UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class); assertNotNull(mock, "Mock对象应该被自动注入");
// 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());}
常见错误:
// ❌ 错误:忘记配置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未正确关闭导致测试干扰 链接到标题
问题表现:
第一个测试类通过,第二个测试类失败
org.mockito.exceptions.base.MockitoException: For com.aliyun.openservices.ons.api.ONSFactory, static mocking is already registered
原因: MockedStatic 对象未在 @AfterAll 中关闭
解决方案:
@SpringBootTest
public abstract class BaseIntegrationTest {
private static MockedStatic<ONSFactory> mockedOnsFactory;
@BeforeAll
public static void initBeforeClass() {
mockedOnsFactory = Mockito.mockStatic(ONSFactory.class); // ... } @AfterAll
public static void afterAll() {
// ⚠️ 必须关闭,否则影响其他测试类
if (mockedOnsFactory != null) { mockedOnsFactory.close();
} }}
8.2.6 多数据源初始化重复执行 链接到标题
问题表现:
数据库初始化日志打印多次
测试数据重复插入导致主键冲突
原因: Spring容器复用时,InitializingBean.afterPropertiesSet() 被多次调用
解决方案:
@Slf4j
public abstract class BaseCommonServiceTest extends BaseIntegrationTest implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
doInitDatasource(); }
void doInitDatasource() {
// ✅ 使用单例标志位,确保只初始化一次
if (TestDataSourceConfig.isDatasourceInitialized()) { log.info("数据源已初始化,跳过");
return; } log.info("开始初始化测试数据库...");
// 初始化逻辑...
TestDataSourceConfig.setDatasourceInitialized();
}}
8.2.7 测试运行速度慢 链接到标题
优化策略:
- 复用Spring容器
// ✅ 所有测试类使用相同的配置
@SpringBootTest(classes = TestCallServiceApplication.class)
@ActiveProfiles("testcase")
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 # 测试环境无需太大
- 使用并行执行
<plugin>
<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0</version> <configuration> <parallel>classes</parallel> <threadCount>4</threadCount> <forkCount>1</forkCount> <reuseForks>true</reuseForks> </configuration></plugin>
- 按需初始化数据源
// 如果测试只用到部分数据源,可以跳过其他数据源初始化
void doInitDatasource() {
if (TestDataSourceConfig.isDatasourceInitialized()) { return; } // 只初始化需要的数据源
initDatabase("dataSourceMaster", "schema/master-schema.sql", "data/master-data.sql"); // 其他数据源按需初始化
TestDataSourceConfig.setDatasourceInitialized();
}
九、从现有测试迁移 链接到标题
9.1 迁移策略 链接到标题
原则: 渐进式迁移,保留原有单元测试,逐步增加集成测试
阶段1: 搭建框架(1-2天)
└─ 创建测试基类和配置
阶段2: 迁移核心流程(1周)
└─ 选择3-5个核心业务流程编写集成测试
阶段3: 补充边界场景(持续)
└─ 根据优先级逐步覆盖其他场景
阶段4: 质量门禁(1天)
└─ 集成到CI流程,设置覆盖率阈值
9.2 从单元测试迁移 链接到标题
场景: 现有大量单元测试使用Mock,希望转为集成测试
迁移前(单元测试):
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderMapper orderMapper;
@Mock
private OrderLineMapper orderLineMapper;
@Mock
private UserRemoteService userRemoteService;
@Mock
private RedisTemplate<String, Object> 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());
}}
迁移后(集成测试):
@SpringBootTest
class OrderServiceIntegrationTest extends BaseCommonServiceTest {
@Autowired
private OrderService orderService;
@Test
@DisplayName("创建订单-完整流程测试")
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("PENDING", result.getStatus()); // 验证数据库真实写入
OrderDTO dbRecord = orderService.getById(result.getId());
assertNotNull(dbRecord); assertEquals(result.getOrderName(), dbRecord.getOrderName()); }}
对比:
| 维度 | 单元测试 | 集成测试 |
|---|---|---|
| Mock数量 | 需要Mock所有依赖(5+) | 只Mock外部服务(1-2个) |
| 代码量 | 50-100行 | 20-30行 |
| 真实性 | 低(所有依赖都是假的) | 高(数据库、Redis真实) |
| 维护成本 | 高(依赖变更需调整Mock) | 低(只关注业务逻辑) |
| 执行速度 | 快(毫秒级) | 较快(秒级,首次启动慢) |
9.3 迁移检查清单 链接到标题
迁移前准备:
- 了解项目的技术栈(Dubbo版本、数据源数量)
- 确认JDK版本(项目使用JDK 1.8)
- 确认Spring Boot版本(建议使用2.x或3.x)
- 评估需要Mock的外部服务(Dubbo、OSS、MQ等)
搭建框架:
- 创建
TestApplication启动类 - 创建
BaseIntegrationTest基类 - 创建
BaseCommonServiceTest服务测试基类 - 配置
TestDataSourceConfig多数据源 - 配置
TestMockConfigDubbo自动Mock - 准备
application-testcase.properties配置文件 - 准备各数据源的
schema.sql和data.sql
编写首个测试:
- 选择一个简单的Service编写集成测试
- 验证Spring容器启动成功
- 验证数据库初始化成功
- 验证Redis连接成功
- 验证Dubbo Mock生效
持续完善:
- 为核心业务流程编写集成测试
- 设置JaCoCo覆盖率目标(80%)
- 集成到Maven构建流程
- 编写测试文档和示例
十、常见问题 链接到标题
10.1 H2与MySQL的SQL差异 链接到标题
10.1.1 H2不支持FORCE INDEX 链接到标题
解决方案: 使用MyBatis拦截器移除
@Intercepts({@Signature(
type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ForceIndexInterceptor implements Interceptor {
private static final Pattern FORCE_INDEX_PATTERN = Pattern.compile("FORCE\\s+INDEX\\s*\\([^)]*\\)", 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("");
// 更新SQL并继续执行
return invocation.proceed(); }}
10.1.2 日期函数差异 链接到标题
-- MySQL
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') FROM orders;
-- H2兼容写法
SELECT FORMATDATETIME(create_time, 'yyyy-MM-dd') FROM orders;
解决方案:
- 使用H2兼容模式:
jdbc:h2:mem:testdb;MODE=MySQL - 创建自定义函数映射
- 使用TestContainers运行真实MySQL
10.2 事务回滚不生效 链接到标题
10.2.1 症状 链接到标题
测试数据未被清理,影响其他测试
10.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, "Mock对象应该存在");
// 配置行为
when(mock.call()).thenReturn("test"); // 验证调用
service.doSomething(); verify(mock, atLeastOnce()).call();}
10.4 并发测试不稳定 链接到标题
10.4.1 问题 链接到标题
并发测试偶尔失败,结果不可预测
解决方案 链接到标题
使用CountDownLatch同步多线程
@Test
void testConcurrentOperation() throws InterruptedException {
int threadCount = 10; CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch endLatch = new CountDownLatch(threadCount); ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<Result>> futures = new ArrayList<>(); for (int i = 0; i < threadCount; i++) {
Future<Result> future = executor.submit(() -> { 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 -> { 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) // 只扫描必要的包 - 并行执行测试
<parallel>classes</parallel> <threadCount>4</threadCount> - 复用Spring容器
- 使用相同的
@SpringBootTest配置 - 使用相同的
@ActiveProfiles
- 使用相同的
十一、附录:实施案例 链接到标题
11.1 项目背景 链接到标题
典型项目概况:
- 业务领域: 典型的企业级业务系统(如订单管理、任务调度等)
- 技术栈:
- 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天) 链接到标题
第一步: 创建测试基类体系
BaseIntegrationTest (基础设施)
└─ BaseCommonServiceTest (数据初始化)
└─ 具体业务测试类
第二步: 配置7个H2内存数据源
// TestDataSourceConfig.java
@Bean(name = "dataSourceMaster")
@Bean(name = "dataSourceSlaveOne")
@Bean(name = "dataSourceSlaveTwo")
@Bean(name = "dataSourceSlaveThree")
@Bean(name = "dataSourceDrdsMaster")
@Bean(name = "dataSourceCsCrm")
@Bean(name = "dataSourceWareHouse")
第三步: 实现Dubbo自动Mock
// TestMockConfig.java
@Component
public class TestMockConfig implements BeanPostProcessor {
// 自动扫描@Reference注解,注入Mock对象
}
第四步: Mock静态工厂类
// BaseIntegrationTest.java
MockedStatic<ONSFactory> mockedOnsFactory; // RocketMQ
MockedStatic<OssClientFactory> mockedOssClientFactory; // OSS
11.3.2 SQL兼容性处理 链接到标题
问题: 项目中大量SQL使用了 FORCE INDEX,H2不支持
解决方案: 实现MyBatis拦截器
@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(""); // ... }}
11.3.3 核心流程测试编写(1周) 链接到标题
选择5个核心业务流程编写集成测试:
- 订单创建流程: 创建订单 → 保存数据库 → 缓存 → 发送MQ
- 数据导入流程: 解析文件 → 批量插入 → 数据校验
- 状态变更流程: 状态机流转 → 触发事件 → 更新统计
- 业务审批流程: 多级审批 → 状态更新 → 通知
- 统计查询: 多数据源聚合 → Redis缓存
示例测试:
@SpringBootTest
public class OrderServiceIntegrationTest extends BaseCommonServiceTest {
@Autowired
private OrderService orderService;
@Test
@DisplayName("订单创建-完整流程测试")
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 = "order:" + result.getId(); assertTrue(redisTemplate.hasKey(cacheKey)); // 5. 验证Dubbo服务调用
verify(userService, times(1)).getUserById(anyLong()); } @Test
@DisplayName("数据导入-批量导入1000条数据")
void testImportData() {
// 准备测试数据
OrderDTO order = createOrder(); List<OrderLineDTO> lines = buildOrderLines(1000); // 执行导入
ImportResult result = orderService.importLines(order.getId(), lines);
// 验证结果
assertEquals(1000, result.getSuccessCount()); assertEquals(0, result.getFailCount()); // 验证数据库
List<OrderLineDTO> 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 质量提升 链接到标题
发现的典型问题:
- 空指针异常: 3处未检查null的代码
- 并发问题: 2处缓存更新竞态条件
- 数据一致性: 4处跨数据源事务未正确处理
- 性能问题: 1处N+1查询
- 边界条件: 2处未处理空列表的情况
11.4.3 开发效率提升 链接到标题
重构信心增强:
- 有完整的集成测试保护,重构时能快速发现问题
- 代码评审时能运行测试验证改动
新功能开发加速:
- 新增功能先写测试,明确需求和边界
- TDD方式开发,减少返工
问题定位快速:
- 线上问题能通过集成测试快速复现
- 修复后运行测试验证
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 推广建议 链接到标题
适合推广的项目特征:
- ✅ 使用Spring Boot 2.x
- ✅ 使用Dubbo或Feign作为RPC框架
- ✅ 有多数据源
- ✅ 依赖外部服务(MQ、OSS等)
- ✅ 业务逻辑复杂,需要全链路测试
不适合的场景:
- ❌ 简单的CRUD应用(过度设计)
- ❌ 对SQL兼容性要求极高(建议用TestContainers)
- ❌ 测试执行时间要求<5秒(首次启动慢)
十二、总结与展望 链接到标题
12.1 核心价值 链接到标题
本框架通过以下设计实现高质量、高效率的集成测试:
环境隔离:
- H2内存数据库,无需外部MySQL
- 内嵌Redis,无需外部Redis
- Mock Dubbo服务,无需注册中心
- 独立测试环境,不干扰生产
快速执行:
- 首次启动15-30秒,后续测试秒级
- Spring容器复用,避免重复启动
- 并行执行,充分利用CPU
自动Mock:
- Dubbo服务自动Mock,减少90%代码
- 静态工厂类集中Mock
- 动态配置Mock行为
易于维护:
- Builder模式构建测试数据
- 清晰的三层架构(基础设施/配置/业务)
- 单元测试和集成测试互补
质量保障:
- JaCoCo覆盖率监控
- 全链路业务流程验证
- 发现潜在Bug
12.2 适用范围 链接到标题
推荐使用的项目:
- Spring Boot 2.x / 3.x 微服务项目
- 使用Dubbo/Feign的分布式系统
- 多数据源业务场景
- 依赖大量外部服务的应用
- 需要快速反馈的敏捷团队
- 强调质量的企业级项目
可简化的场景:
- 简单的CRUD应用(可能过度设计)
- 对数据库SQL兼容性要求极高(建议使用TestContainers)
- 测试执行时间要求<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 持续改进方向 链接到标题
契约测试:
- 使用Pact验证Dubbo服务契约
- 确保服务间接口一致性
混合策略:
- 常规测试使用H2
- 关键流程使用TestContainers真实MySQL
- 性能测试使用生产环境数据
测试数据工厂:
- 统一的测试数据生成器
- 快速生成各种场景数据
- 支持数据变体生成
可视化报告:
- 集成Allure生成美观的测试报告
- 测试执行趋势分析
- 覆盖率变化监控
智能化测试:
- 基于代码变更自动生成测试用例
- 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数据源:
spring.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:
redisServer = new RedisServerBuilder()
.bind("localhost") .port(6380) .setting("requirepass <password>") .build();
Dubbo Mock:
UserRemoteService mock = TestMockConfig.getMock(UserRemoteService.class);
when(mock.getUserById(anyLong())).thenReturn(buildMockUser());
静态工厂Mock:
MockedStatic<ONSFactory> mockedFactory = Mockito.mockStatic(ONSFactory.class);
mockedFactory.when(() -> 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("测试业务场景-描述预期结果")
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 + 多数据源的企业级项目 维护者: 测试框架团队