初始化
This commit is contained in:
373
rdms-framework/rdms-spring-boot-starter-test/README.md
Normal file
373
rdms-framework/rdms-spring-boot-starter-test/README.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# rdms-spring-boot-starter-test
|
||||
|
||||
## 1. 模块定位
|
||||
|
||||
`rdms-spring-boot-starter-test` 是项目中的测试基础模块,作用不是提供业务测试用例,而是为测试提供统一的基础设施和基类。
|
||||
|
||||
当前模块主要解决的是:
|
||||
|
||||
- 纯 Mockito 单元测试怎么写
|
||||
- 依赖数据库的单元测试怎么快速启动
|
||||
- 依赖 Redis 的单元测试怎么快速启动
|
||||
- 同时依赖 DB 和 Redis 的测试怎么快速启动
|
||||
- 测试里怎么更方便地造随机数据和做断言
|
||||
|
||||
所以这个模块更适合理解为“测试脚手架模块”。
|
||||
|
||||
## 2. 设计思路
|
||||
|
||||
当前模块的设计思路比较明确:把常见测试场景抽成几种固定模板,让开发人员通过继承基类快速开始写测试,而不是每个模块都自己从零搭测试环境。
|
||||
|
||||
整体上分成三层:
|
||||
|
||||
1. 依赖层
|
||||
统一引入测试常用依赖,例如 Mockito、Spring Boot Test、H2、内存 Redis、随机对象生成工具。
|
||||
|
||||
2. 配置层
|
||||
提供针对测试场景的补充配置,例如:
|
||||
- 内存 Redis 启动配置
|
||||
- SQL 初始化配置
|
||||
|
||||
3. 基类层
|
||||
把测试场景收口成几类基类,开发时按场景继承即可。
|
||||
|
||||
这种设计的重点不是“测试能力多复杂”,而是“让测试环境标准化”。
|
||||
|
||||
## 3. 当前提供的能力
|
||||
|
||||
### 3.1 纯 Mockito 单元测试基类
|
||||
|
||||
基类:
|
||||
|
||||
- `BaseMockitoUnitTest`
|
||||
|
||||
作用:
|
||||
|
||||
- 适合不需要 Spring 容器、不需要 DB、不需要 Redis 的纯单元测试
|
||||
- 基于 `MockitoExtension`
|
||||
|
||||
适用场景:
|
||||
|
||||
- 测试工具类
|
||||
- 测试纯业务逻辑类
|
||||
- 依赖全部可以通过 Mock 替代的 Service
|
||||
|
||||
### 3.2 内存 DB 单元测试基类
|
||||
|
||||
基类:
|
||||
|
||||
- `BaseDbUnitTest`
|
||||
|
||||
作用:
|
||||
|
||||
- 启动 Spring 测试上下文
|
||||
- 引入数据源、事务、MyBatis 相关配置
|
||||
- 使用 H2 作为内存数据库
|
||||
- 支持 SQL 初始化
|
||||
- 每个测试方法结束后自动执行 `/sql/clean.sql` 清理数据库
|
||||
|
||||
适用场景:
|
||||
|
||||
- Mapper 测试
|
||||
- 依赖本模块数据库访问的 Service 测试
|
||||
- 需要真实执行 MyBatis SQL 的测试
|
||||
|
||||
### 3.3 内存 Redis 单元测试基类
|
||||
|
||||
基类:
|
||||
|
||||
- `BaseRedisUnitTest`
|
||||
|
||||
作用:
|
||||
|
||||
- 启动 Spring 测试上下文
|
||||
- 启动内存 Redis
|
||||
- 引入 Redis 与 Redisson 相关配置
|
||||
|
||||
适用场景:
|
||||
|
||||
- Redis DAO 测试
|
||||
- 缓存逻辑测试
|
||||
- 分布式锁、限流、缓存等依赖 Redis 的逻辑测试
|
||||
|
||||
### 3.4 内存 DB + Redis 组合测试基类
|
||||
|
||||
基类:
|
||||
|
||||
- `BaseDbAndRedisUnitTest`
|
||||
|
||||
作用:
|
||||
|
||||
- 同时启动内存数据库和内存 Redis
|
||||
- 一次性引入 DB、MyBatis、Redis、Redisson 所需配置
|
||||
- 每个测试方法结束后自动清理数据库
|
||||
|
||||
适用场景:
|
||||
|
||||
- 同时依赖数据库和缓存的 Service 测试
|
||||
- 需要验证“写库 + 删缓存 / 刷缓存”这类联动逻辑的测试
|
||||
|
||||
### 3.5 内存 Redis 启动配置
|
||||
|
||||
配置类:
|
||||
|
||||
- `RedisTestConfiguration`
|
||||
|
||||
作用:
|
||||
|
||||
- 基于 `jedis-mock` 启动一个内存 Redis Server
|
||||
- 供 Redis 相关测试基类复用
|
||||
|
||||
这个配置的重点是:让测试不依赖外部真实 Redis 服务。
|
||||
|
||||
### 3.6 SQL 初始化配置
|
||||
|
||||
配置类:
|
||||
|
||||
- `SqlInitializationTestConfiguration`
|
||||
|
||||
作用:
|
||||
|
||||
- 在测试场景下补充 DataSource SQL 初始化能力
|
||||
- 解决延迟加载场景下默认 SQL 初始化配置不生效的问题
|
||||
|
||||
这个配置的核心价值是:保证 H2 测试数据库能按测试配置正常初始化 schema 和 data。
|
||||
|
||||
### 3.7 随机测试数据工具
|
||||
|
||||
工具类:
|
||||
|
||||
- `RandomUtils`
|
||||
|
||||
作用:
|
||||
|
||||
- 随机生成字符串、数字、时间、邮箱、手机号
|
||||
- 随机生成 POJO
|
||||
- 随机生成对象列表、对象集合
|
||||
|
||||
它内部基于 `podam` 做随机对象填充,并对部分字段做了定制处理,例如:
|
||||
|
||||
- `status` 字段优先生成常见状态值
|
||||
- `deleted` 字段默认生成 `false`
|
||||
- `LocalDateTime` 的纳秒位归零,避免 MySQL / H2 时间精度差异
|
||||
|
||||
### 3.8 测试断言工具
|
||||
|
||||
工具类:
|
||||
|
||||
- `AssertUtils`
|
||||
|
||||
作用:
|
||||
|
||||
- 对比两个 POJO 的字段是否一致
|
||||
- 断言是否抛出指定 `ServiceException`
|
||||
|
||||
适用场景:
|
||||
|
||||
- 校验 DTO / DO / VO 转换结果
|
||||
- 校验业务异常是否符合预期
|
||||
|
||||
## 4. 开发人员怎么上手
|
||||
|
||||
这个模块最重要的不是“看懂代码”,而是先判断当前测试属于哪一类,然后继承对应基类。
|
||||
|
||||
### 4.1 如果只是纯逻辑测试
|
||||
|
||||
直接继承:
|
||||
|
||||
```java
|
||||
class XxxServiceTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- 不需要 Spring 容器
|
||||
- 不访问数据库
|
||||
- 不访问 Redis
|
||||
- 依赖都可以 Mock
|
||||
|
||||
这是成本最低、执行最快的一类测试。
|
||||
|
||||
### 4.2 如果要测 Mapper 或真实 SQL
|
||||
|
||||
直接继承:
|
||||
|
||||
```java
|
||||
class XxxMapperTest extends BaseDbUnitTest {
|
||||
}
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- Mapper 层测试
|
||||
- 依赖 H2 跑 SQL
|
||||
- Service 里要真实查库、写库
|
||||
|
||||
开发时通常要准备:
|
||||
|
||||
- `application-unit-test.yaml`
|
||||
- 初始化 SQL
|
||||
- `clean.sql`
|
||||
|
||||
### 4.3 如果要测 Redis 逻辑
|
||||
|
||||
直接继承:
|
||||
|
||||
```java
|
||||
class XxxRedisDaoTest extends BaseRedisUnitTest {
|
||||
}
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- Redis Key 操作测试
|
||||
- 缓存逻辑测试
|
||||
- 基于 Redis 的基础设施测试
|
||||
|
||||
这样就不需要自己额外准备 Redis 环境。
|
||||
|
||||
### 4.4 如果业务同时依赖 DB 和 Redis
|
||||
|
||||
直接继承:
|
||||
|
||||
```java
|
||||
class XxxServiceTest extends BaseDbAndRedisUnitTest {
|
||||
}
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- 先写库再删缓存
|
||||
- 先查库再写缓存
|
||||
- 依赖数据库和 Redis 共同完成的业务逻辑
|
||||
|
||||
### 4.5 如果想快速造测试对象
|
||||
|
||||
可以直接使用 `RandomUtils`:
|
||||
|
||||
```java
|
||||
UserSaveReqVO reqVO = RandomUtils.randomPojo(UserSaveReqVO.class);
|
||||
List<UserSaveReqVO> list = RandomUtils.randomPojoList(UserSaveReqVO.class, 3);
|
||||
String email = RandomUtils.randomEmail();
|
||||
String mobile = RandomUtils.randomMobile();
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- 减少手写测试数据
|
||||
- 快速生成大量测试输入
|
||||
- 配合局部字段覆盖提高测试效率
|
||||
|
||||
### 4.6 如果要断言业务异常
|
||||
|
||||
可以直接使用 `AssertUtils.assertServiceException(...)`:
|
||||
|
||||
```java
|
||||
AssertUtils.assertServiceException(
|
||||
() -> userService.createUser(reqVO),
|
||||
USER_USERNAME_EXISTS
|
||||
);
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- 校验业务校验逻辑
|
||||
- 校验异常码和异常信息
|
||||
|
||||
### 4.7 如果要断言对象字段一致
|
||||
|
||||
可以使用:
|
||||
|
||||
```java
|
||||
AssertUtils.assertPojoEquals(expected, actual, "createTime", "updateTime");
|
||||
```
|
||||
|
||||
适合:
|
||||
|
||||
- 校验转换逻辑
|
||||
- 校验查询结果
|
||||
- 忽略少数字段差异后做整体对比
|
||||
|
||||
## 5. 常见使用建议
|
||||
|
||||
### 5.1 优先选最轻的测试基类
|
||||
|
||||
建议顺序是:
|
||||
|
||||
1. 能用 `BaseMockitoUnitTest` 就不要上 Spring 容器
|
||||
2. 需要真实 DB 再用 `BaseDbUnitTest`
|
||||
3. 需要真实 Redis 再用 `BaseRedisUnitTest`
|
||||
4. 两者都需要时再用 `BaseDbAndRedisUnitTest`
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 测试越轻,执行越快
|
||||
- 环境越少,问题越少
|
||||
|
||||
### 5.2 本模块更偏“单元测试基础设施”
|
||||
|
||||
虽然这里用到了 Spring Boot Test、H2、内存 Redis,但当前命名和设计仍然偏“单元测试 / 轻量集成测试”。
|
||||
|
||||
也就是说,它更适合:
|
||||
|
||||
- 在模块内部验证逻辑正确性
|
||||
- 快速验证 Mapper / Redis / Service 行为
|
||||
|
||||
而不是:
|
||||
|
||||
- 完整端到端测试
|
||||
- 完整微服务联调测试
|
||||
- 依赖真实外部中间件的系统测试
|
||||
|
||||
### 5.3 自己模块真实依赖走内存实现,外部模块依赖走 Mock
|
||||
|
||||
从 `BaseDbUnitTest` 的注释就能看出当前设计思路:
|
||||
|
||||
- 自己模块的 Mapper 走 H2
|
||||
- 别的模块的 Service 走 Mock
|
||||
|
||||
这是一种比较务实的测试策略:
|
||||
|
||||
- 保留自己模块的真实数据访问能力
|
||||
- 避免跨模块依赖把测试拖重
|
||||
|
||||
## 6. 模块边界
|
||||
|
||||
当前模块负责的是:
|
||||
|
||||
- 提供测试基类
|
||||
- 提供测试配置
|
||||
- 提供随机数据和断言工具
|
||||
|
||||
当前模块不负责的是:
|
||||
|
||||
- 自动生成业务测试用例
|
||||
- 自动替你写 Mock 行为
|
||||
- 真实中间件联调
|
||||
- 完整集成测试平台
|
||||
|
||||
所以它的职责很明确:让测试环境更容易搭,不是替业务写测试。
|
||||
|
||||
## 7. 总结
|
||||
|
||||
`rdms-spring-boot-starter-test` 当前已经把项目里最常见的测试场景做成了统一模板。
|
||||
|
||||
对于开发人员来说,真正重要的上手方式只有两步:
|
||||
|
||||
1. 先判断当前测试依赖什么环境
|
||||
2. 再继承对应的基类
|
||||
|
||||
如果只是想快速理解这个模块,可以直接记住下面这张映射:
|
||||
|
||||
- 纯逻辑测试:`BaseMockitoUnitTest`
|
||||
- 测 DB:`BaseDbUnitTest`
|
||||
- 测 Redis:`BaseRedisUnitTest`
|
||||
- DB + Redis 一起测:`BaseDbAndRedisUnitTest`
|
||||
|
||||
再配合:
|
||||
|
||||
- `RandomUtils` 造数据
|
||||
- `AssertUtils` 做断言
|
||||
|
||||
基本就能覆盖大部分日常测试开发场景。
|
||||
61
rdms-framework/rdms-spring-boot-starter-test/pom.xml
Normal file
61
rdms-framework/rdms-spring-boot-starter-test/pom.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rdms-spring-boot-starter-test</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>测试组件,用于单元测试、集成测试</description>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId> <!-- 单元测试,我们采用 H2 作为数据库 -->
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
|
||||
<artifactId>jedis-mock</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
|
||||
<artifactId>podam</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.framework.test.config;
|
||||
|
||||
import com.github.fppt.jedismock.RedisServer;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Redis 测试 Configuration,主要实现内嵌 Redis 的启动
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Lazy(false) // 禁止延迟加载
|
||||
@EnableConfigurationProperties(RedisProperties.class)
|
||||
public class RedisTestConfiguration {
|
||||
|
||||
/**
|
||||
* 创建模拟的 Redis Server 服务器
|
||||
*/
|
||||
@Bean
|
||||
public RedisServer redisServer(RedisProperties properties) throws IOException {
|
||||
RedisServer redisServer = new RedisServer(properties.getPort());
|
||||
// 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
|
||||
try {
|
||||
redisServer.start();
|
||||
} catch (Exception ignore) {}
|
||||
return redisServer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.njcn.rdms.framework.test.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
|
||||
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
|
||||
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* SQL 初始化的测试 Configuration
|
||||
*
|
||||
* 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
|
||||
* 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化
|
||||
* 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
|
||||
@ConditionalOnSingleCandidate(DataSource.class)
|
||||
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
|
||||
@Lazy(value = false) // 禁止延迟加载
|
||||
@EnableConfigurationProperties(SqlInitializationProperties.class)
|
||||
public class SqlInitializationTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
|
||||
SqlInitializationProperties initializationProperties) {
|
||||
DatabaseInitializationSettings settings = createFrom(initializationProperties);
|
||||
return new DataSourceScriptDatabaseInitializer(dataSource, settings);
|
||||
}
|
||||
|
||||
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
|
||||
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
|
||||
settings.setSchemaLocations(properties.getSchemaLocations());
|
||||
settings.setDataLocations(properties.getDataLocations());
|
||||
settings.setContinueOnError(properties.isContinueOnError());
|
||||
settings.setSeparator(properties.getSeparator());
|
||||
settings.setEncoding(properties.getEncoding());
|
||||
settings.setMode(properties.getMode());
|
||||
return settings;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.framework.test.core.ut;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration;
|
||||
import com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration;
|
||||
import com.njcn.rdms.framework.redis.config.RdmsRedisAutoConfiguration;
|
||||
import com.njcn.rdms.framework.test.config.RedisTestConfiguration;
|
||||
import com.njcn.rdms.framework.test.config.SqlInitializationTestConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
/**
|
||||
* 依赖内存 DB + Redis 的单元测试
|
||||
*
|
||||
* 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
|
||||
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
|
||||
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
|
||||
public class BaseDbAndRedisUnitTest {
|
||||
|
||||
@Import({
|
||||
// DB 配置类
|
||||
RdmsDataSourceAutoConfiguration.class, // 自己的 DB 配置类
|
||||
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
|
||||
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
|
||||
DruidDataSourceAutoConfigure.class, // Druid 自动配置类
|
||||
SqlInitializationTestConfiguration.class, // SQL 初始化
|
||||
// MyBatis 配置类
|
||||
RdmsMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
|
||||
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
|
||||
|
||||
// Redis 配置类
|
||||
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
|
||||
RdmsRedisAutoConfiguration.class, // 自己的 Redis 配置类
|
||||
RedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
RedissonAutoConfiguration.class, // Redisson 自动配置类
|
||||
|
||||
// 其它配置类
|
||||
SpringUtil.class
|
||||
})
|
||||
public static class Application {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.njcn.rdms.framework.test.core.ut;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
|
||||
import com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration;
|
||||
import com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration;
|
||||
import com.njcn.rdms.framework.test.config.SqlInitializationTestConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
/**
|
||||
* 依赖内存 DB 的单元测试
|
||||
*
|
||||
* 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
|
||||
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
|
||||
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
|
||||
public class BaseDbUnitTest {
|
||||
|
||||
@Import({
|
||||
// DB 配置类
|
||||
RdmsDataSourceAutoConfiguration.class, // 自己的 DB 配置类
|
||||
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
|
||||
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
|
||||
DruidDataSourceAutoConfigure.class, // Druid 自动配置类
|
||||
SqlInitializationTestConfiguration.class, // SQL 初始化
|
||||
// MyBatis 配置类
|
||||
RdmsMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
|
||||
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
|
||||
MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
|
||||
|
||||
// 其它配置类
|
||||
SpringUtil.class
|
||||
})
|
||||
public static class Application {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.njcn.rdms.framework.test.core.ut;
|
||||
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* 纯 Mockito 的单元测试
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class BaseMockitoUnitTest {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.njcn.rdms.framework.test.core.ut;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.njcn.rdms.framework.redis.config.RdmsRedisAutoConfiguration;
|
||||
import com.njcn.rdms.framework.test.config.RedisTestConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
/**
|
||||
* 依赖内存 Redis 的单元测试
|
||||
*
|
||||
* 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class)
|
||||
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
|
||||
public class BaseRedisUnitTest {
|
||||
|
||||
@Import({
|
||||
// Redis 配置类
|
||||
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
|
||||
RedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
RdmsRedisAutoConfiguration.class, // 自己的 Redis 配置类
|
||||
RedissonAutoConfiguration.class, // Redisson 自动配置类
|
||||
|
||||
// 其它配置类
|
||||
SpringUtil.class
|
||||
})
|
||||
public static class Application {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 提供单元测试 Unit Test 的基类
|
||||
*/
|
||||
package com.njcn.rdms.framework.test.core.ut;
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.njcn.rdms.framework.test.core.util;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.njcn.rdms.framework.common.exception.ErrorCode;
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* 单元测试,assert 断言工具类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class AssertUtils {
|
||||
|
||||
/**
|
||||
* 比对两个对象的属性是否一致
|
||||
*
|
||||
* 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
|
||||
*
|
||||
* @param expected 期望对象
|
||||
* @param actual 实际对象
|
||||
* @param ignoreFields 忽略的属性数组
|
||||
*/
|
||||
public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
|
||||
Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
|
||||
Arrays.stream(expectedFields).forEach(expectedField -> {
|
||||
// 忽略 jacoco 自动生成的 $jacocoData 属性的情况
|
||||
if (expectedField.isSynthetic()) {
|
||||
return;
|
||||
}
|
||||
// 如果是忽略的属性,则不进行比对
|
||||
if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
|
||||
return;
|
||||
}
|
||||
// 忽略不存在的属性
|
||||
Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
|
||||
if (actualField == null) {
|
||||
return;
|
||||
}
|
||||
// 比对
|
||||
Assertions.assertEquals(
|
||||
ReflectUtil.getFieldValue(expected, expectedField),
|
||||
ReflectUtil.getFieldValue(actual, actualField),
|
||||
String.format("Field(%s) 不匹配", expectedField.getName())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 比对两个对象的属性是否一致
|
||||
*
|
||||
* 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
|
||||
*
|
||||
* @param expected 期望对象
|
||||
* @param actual 实际对象
|
||||
* @param ignoreFields 忽略的属性数组
|
||||
* @return 是否一致
|
||||
*/
|
||||
public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) {
|
||||
Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
|
||||
return Arrays.stream(expectedFields).allMatch(expectedField -> {
|
||||
// 如果是忽略的属性,则不进行比对
|
||||
if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
|
||||
return true;
|
||||
}
|
||||
// 忽略不存在的属性
|
||||
Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
|
||||
if (actualField == null) {
|
||||
return true;
|
||||
}
|
||||
return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField),
|
||||
ReflectUtil.getFieldValue(actual, actualField));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行方法,校验抛出的 Service 是否符合条件
|
||||
*
|
||||
* @param executable 业务异常
|
||||
* @param errorCode 错误码对象
|
||||
* @param messageParams 消息参数
|
||||
*/
|
||||
public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) {
|
||||
// 调用方法
|
||||
ServiceException serviceException = assertThrows(ServiceException.class, executable);
|
||||
// 校验错误码
|
||||
Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配");
|
||||
String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams);
|
||||
Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.njcn.rdms.framework.test.core.util;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
|
||||
import uk.co.jemos.podam.api.PodamFactory;
|
||||
import uk.co.jemos.podam.api.PodamFactoryImpl;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 随机工具类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class RandomUtils {
|
||||
|
||||
private static final int RANDOM_STRING_LENGTH = 10;
|
||||
|
||||
private static final int TINYINT_MAX = 127;
|
||||
|
||||
private static final int RANDOM_DATE_MAX = 30;
|
||||
|
||||
private static final int RANDOM_COLLECTION_LENGTH = 5;
|
||||
|
||||
private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl();
|
||||
|
||||
static {
|
||||
// 字符串
|
||||
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class,
|
||||
(dataProviderStrategy, attributeMetadata, map) -> randomString());
|
||||
// Integer
|
||||
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> {
|
||||
// 如果是 status 的字段,返回 0 或 1
|
||||
if ("status".equals(attributeMetadata.getAttributeName())) {
|
||||
return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
|
||||
}
|
||||
// 如果是 type、status 结尾的字段,返回 tinyint 范围
|
||||
if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(),
|
||||
"type", "status", "category", "scope", "result")) {
|
||||
return RandomUtil.randomInt(0, TINYINT_MAX + 1);
|
||||
}
|
||||
return RandomUtil.randomInt();
|
||||
});
|
||||
// LocalDateTime
|
||||
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
|
||||
(dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());
|
||||
// Boolean
|
||||
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> {
|
||||
// 如果是 deleted 的字段,返回非删除
|
||||
if ("deleted".equals(attributeMetadata.getAttributeName())) {
|
||||
return false;
|
||||
}
|
||||
return RandomUtil.randomBoolean();
|
||||
});
|
||||
}
|
||||
|
||||
public static String randomString() {
|
||||
return RandomUtil.randomString(RANDOM_STRING_LENGTH);
|
||||
}
|
||||
|
||||
public static Long randomLongId() {
|
||||
return RandomUtil.randomLong(0, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static Integer randomInteger() {
|
||||
return RandomUtil.randomInt(0, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static Date randomDate() {
|
||||
return RandomUtil.randomDay(0, RANDOM_DATE_MAX);
|
||||
}
|
||||
|
||||
public static LocalDateTime randomLocalDateTime() {
|
||||
// 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳
|
||||
return LocalDateTimeUtil.of(randomDate()).withNano(0);
|
||||
}
|
||||
|
||||
public static Short randomShort() {
|
||||
return (short) RandomUtil.randomInt(0, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static <T> Set<T> randomSet(Class<T> clazz) {
|
||||
return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH))
|
||||
.map(i -> randomPojo(clazz)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static Integer randomCommonStatus() {
|
||||
return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
|
||||
}
|
||||
|
||||
public static String randomEmail() {
|
||||
return randomString() + "@qq.com";
|
||||
}
|
||||
|
||||
public static String randomMobile() {
|
||||
return "13800138" + RandomUtil.randomNumbers(3);
|
||||
}
|
||||
|
||||
public static String randomURL() {
|
||||
return "https://www.iocoder.cn/" + randomString();
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> T randomPojo(Class<T> clazz, Consumer<T>... consumers) {
|
||||
T pojo = PODAM_FACTORY.manufacturePojo(clazz);
|
||||
// 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理
|
||||
if (ArrayUtil.isNotEmpty(consumers)) {
|
||||
Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
|
||||
}
|
||||
return pojo;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> T randomPojo(Class<T> clazz, Type type, Consumer<T>... consumers) {
|
||||
T pojo = PODAM_FACTORY.manufacturePojo(clazz, type);
|
||||
// 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理
|
||||
if (ArrayUtil.isNotEmpty(consumers)) {
|
||||
Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
|
||||
}
|
||||
return pojo;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
|
||||
int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH);
|
||||
return randomPojoList(clazz, size, consumers);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> List<T> randomPojoList(Class<T> clazz, int size, Consumer<T>... consumers) {
|
||||
return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user