Spring Boot 集成测试框架搭建指南 链接到标题

🎯 目标: 提供一套通用、高效、易维护的集成测试框架,适用于任何 Spring Boot 项目

📚 目录 链接到标题


一、快速开始 链接到标题

1.1 环境要求 链接到标题

在开始前,请确认以下环境:

组件版本要求当前项目版本说明
JDK≥ 1.81.8必需
Spring Boot2.x2.1.x (企业定制)企业定制版本
Maven≥ 3.6-构建工具
H2 Database2.x依赖于Spring Boot版本内存数据库
MyBatis3.5.x3.5.7持久层框架
Dynamic DataSource3.x3.6.0多数据源支持
Mockito Inline4.x+内置支持静态Mock
Embedded Redis0.7.30.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)
  • 使用 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<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 - 测试基类 链接到标题

职责: 提供测试运行的基础环境和生命周期管理

核心功能:

  1. Spring Boot 测试环境启动
  2. 内嵌服务管理(Redis、MQ等)
  3. 静态服务Mock(工厂类、配置类)
  4. 资源生命周期管理

实现模板:

@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;    }}

配置要点:

  1. 使用Druid连接池: 与生产环境保持一致
  2. @Primary注解: 标记主数据源,避免注入歧义
  3. 单例标志位: 防止多次初始化(Spring容器复用时)
  4. 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...

排查步骤:

  1. 检查 TestCallServiceApplicationexclude 配置
  2. 确认测试配置类被正确扫描
  3. 检查是否有循环依赖

解决方案:

// ✅ 正确的配置
@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

排查清单:

  1. 确认 TestMockConfig 被Spring扫描
  2. 确认字段上有 @Reference 注解
  3. 确认在测试方法中调用了 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 测试运行速度慢 链接到标题

优化策略:

  1. 复用Spring容器
// ✅ 所有测试类使用相同的配置
@SpringBootTest(classes = TestCallServiceApplication.class)
@ActiveProfiles("testcase")
public abstract class BaseIntegrationTest {
}

// Spring会自动复用容器,避免重复启动
  1. 减少数据库连接池大小
# application-testcase.properties
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-active=5  # 测试环境无需太大
  1. 使用并行执行
<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>
  1. 按需初始化数据源
// 如果测试只用到部分数据源,可以跳过其他数据源初始化
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 多数据源
  • 配置 TestMockConfig Dubbo自动Mock
  • 准备 application-testcase.properties 配置文件
  • 准备各数据源的 schema.sqldata.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;

解决方案:

  1. 使用H2兼容模式: jdbc:h2:mem:testdb;MODE=MySQL
  2. 创建自定义函数映射
  3. 使用TestContainers运行真实MySQL

10.2 事务回滚不生效 链接到标题

10.2.1 症状 链接到标题

测试数据未被清理,影响其他测试

10.2.2 原因 链接到标题

  1. 缺少@Transactional注解
  2. 方法内部开启了新事务
  3. 使用了@Commit而非@Rollback

解决方案 链接到标题

// ✅ 正确写法
@Transactional
@Rollback
@Test
void testCreate() {
    service.create(entity);    // 测试结束自动回滚
}

// ❌ 错误写法
@Test
void testCreate() {
    service.create(entity);    // 不会回滚!
}

10.3 Mock未生效 链接到标题

10.3.1 排查清单 链接到标题

  1. 确认TestMockConfig已被Spring扫描
  2. 确认字段上有@Reference@FeignClient注解
  3. 确认Mock对象已注册到MockRegistry
  4. 在测试方法中验证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 优化策略 链接到标题

  1. 使用H2而非真实数据库
    # H2 (快)
    spring.datasource.url=jdbc:h2:mem:testdb      # MySQL (慢)
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    
  2. 减少不必要的组件扫描
    @SpringBootTest(classes = TestApplication.class)
    // 只扫描必要的包
    
  3. 并行执行测试
    <parallel>classes</parallel>
    <threadCount>4</threadCount>
    
  4. 复用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 实施前的挑战 链接到标题

  1. 测试覆盖率低: 仅30%,大量核心业务逻辑未覆盖
  2. 测试编写困难: 需要手动Mock 10+个依赖,每个测试50-100行
  3. 测试不稳定: 依赖外部环境(MySQL、Redis、Dubbo注册中心)
  4. 执行速度慢: 需要启动完整服务,测试执行5分钟+
  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个核心业务流程编写集成测试:

  1. 订单创建流程: 创建订单 → 保存数据库 → 缓存 → 发送MQ
  2. 数据导入流程: 解析文件 → 批量插入 → 数据校验
  3. 状态变更流程: 状态机流转 → 触发事件 → 更新统计
  4. 业务审批流程: 多级审批 → 状态更新 → 通知
  5. 统计查询: 多数据源聚合 → 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 质量提升 链接到标题

发现的典型问题:

  1. 空指针异常: 3处未检查null的代码
  2. 并发问题: 2处缓存更新竞态条件
  3. 数据一致性: 4处跨数据源事务未正确处理
  4. 性能问题: 1处N+1查询
  5. 边界条件: 2处未处理空列表的情况

11.4.3 开发效率提升 链接到标题

重构信心增强:

  • 有完整的集成测试保护,重构时能快速发现问题
  • 代码评审时能运行测试验证改动

新功能开发加速:

  • 新增功能先写测试,明确需求和边界
  • TDD方式开发,减少返工

问题定位快速:

  • 线上问题能通过集成测试快速复现
  • 修复后运行测试验证

11.5 经验总结 链接到标题

11.5.1 成功关键因素 链接到标题

  1. 自动化Mock: TestMockConfig自动处理Dubbo服务Mock,减少90%的Mock代码
  2. 多数据源方案: 7个H2数据库完全模拟生产环境
  3. SQL兼容拦截器: 解决FORCE INDEX等MySQL特有语法
  4. Builder模式: 简化测试数据构建
  5. 单例初始化: 防止多次初始化数据库,提升速度

11.5.2 避坑指南 链接到标题

  1. MockedStatic必须关闭: 忘记在@AfterAll中close()会导致其他测试失败
  2. 事务回滚: 忘记添加@Transactional导致测试数据残留
  3. 端口冲突: Redis 6380端口被占用导致启动失败
  4. Spring容器复用: 不同配置会导致容器重启,降低速度
  5. H2兼容性: 部分MySQL语法需要适配

11.5.3 推广建议 链接到标题

适合推广的项目特征:

  • ✅ 使用Spring Boot 2.x
  • ✅ 使用Dubbo或Feign作为RPC框架
  • ✅ 有多数据源
  • ✅ 依赖外部服务(MQ、OSS等)
  • ✅ 业务逻辑复杂,需要全链路测试

不适合的场景:

  • ❌ 简单的CRUD应用(过度设计)
  • ❌ 对SQL兼容性要求极高(建议用TestContainers)
  • ❌ 测试执行时间要求<5秒(首次启动慢)

十二、总结与展望 链接到标题

12.1 核心价值 链接到标题

本框架通过以下设计实现高质量、高效率的集成测试:

  1. 环境隔离:

    • H2内存数据库,无需外部MySQL
    • 内嵌Redis,无需外部Redis
    • Mock Dubbo服务,无需注册中心
    • 独立测试环境,不干扰生产
  2. 快速执行:

    • 首次启动15-30秒,后续测试秒级
    • Spring容器复用,避免重复启动
    • 并行执行,充分利用CPU
  3. 自动Mock:

    • Dubbo服务自动Mock,减少90%代码
    • 静态工厂类集中Mock
    • 动态配置Mock行为
  4. 易于维护:

    • Builder模式构建测试数据
    • 清晰的三层架构(基础设施/配置/业务)
    • 单元测试和集成测试互补
  5. 质量保障:

    • JaCoCo覆盖率监控
    • 全链路业务流程验证
    • 发现潜在Bug

12.2 适用范围 链接到标题

推荐使用的项目:

  • Spring Boot 2.x / 3.x 微服务项目
  • 使用Dubbo/Feign的分布式系统
  • 多数据源业务场景
  • 依赖大量外部服务的应用
  • 需要快速反馈的敏捷团队
  • 强调质量的企业级项目

可简化的场景:

  • 简单的CRUD应用(可能过度设计)
  • 对数据库SQL兼容性要求极高(建议使用TestContainers)
  • 测试执行时间要求<5秒的场景

12.3 版本兼容性 链接到标题

组件当前项目版本兼容版本范围说明
JDK1.81.8 - 17JDK 17+需调整反射相关代码
Spring Boot2.1.x2.x - 3.x3.x需改为jakarta包
MyBatis3.5.73.5.x-
Dubbo2.7.x2.6.x - 3.x3.x注解包名变更
Dynamic DataSource3.6.03.x-
H2 Database2.x2.xMySQL兼容模式
Mockito4.x4.x - 5.x需mockito-inline支持静态Mock
Embedded Redis0.7.30.7.xWindows需特殊配置

12.4 持续改进方向 链接到标题

  1. 契约测试:

    • 使用Pact验证Dubbo服务契约
    • 确保服务间接口一致性
  2. 混合策略:

    • 常规测试使用H2
    • 关键流程使用TestContainers真实MySQL
    • 性能测试使用生产环境数据
  3. 测试数据工厂:

    • 统一的测试数据生成器
    • 快速生成各种场景数据
    • 支持数据变体生成
  4. 可视化报告:

    • 集成Allure生成美观的测试报告
    • 测试执行趋势分析
    • 覆盖率变化监控
  5. 智能化测试:

    • 基于代码变更自动生成测试用例
    • AI辅助编写断言
    • 自动识别需要Mock的依赖

12.5 参考资源 链接到标题


十三、快速参考 链接到标题

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 + 多数据源的企业级项目 维护者: 测试框架团队