注意:本篇教程是采用Junit的写法进行举例的,在单测实践过程中,为了验证不同框架混合开发可行性,导致不同层使用的框架不同,还未进行统一。
一、数据层单元测试
关于数据层的单元测试,为了遵从单元测试的原则,可能我们需要脱离数据库依赖;假设我们想这么做,那么就按照前面讲的业务逻辑层单元测试的方法,我们同样把数据库依赖Mock掉就可以了。
但是,如果我们这么做,那么大多情况下我们的数据层是没有什么逻辑的,数据依赖都被Mock了,那么单测可验证的东西就太少了,所以我建议可以依赖测试数据库或者是依赖内存型数据库H2进行单元测试,这样我们就可以快速的验证编写的Sql是否有问题,也可以依赖数据库中的数据驱动测试的执行。
二、数据层单测写法
我们只测试数据层,一定不要使用@SpringBootTest注解,在这里我们认识一个新的注解@MybatisTest,使用这个注解编写单元测试可以达到我们只加载数据层Bean的目的,不用加载Spring项目整体上下文。下面通过示例来看一下如何使用:
引入依赖
<!-- @MybatisTest的依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
新建单测环境配置文件
pom配置文件中指定了默认启用的环境为dev配置(也可以单测中用@ActiveProfiles注解指定profile),所以单测在test文件夹下的resources目录新建一个application-dev.properties的配置文件即可默认加载。配置文件中指定单测使用的测试数据库,同时在集成Bamboo平台时流水线的绑定资源池选择测试环境即可连通
<!-- ################################ 开发,测试,生产环境配置 Start #####################-->
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<env>dev</env>
</properties>
</profile>
<!-- 预发测试环境 -->
<profile>
<id>yf</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<env>yf</env>
</properties>
</profile>
<!-- 生产环境 -->
<profile>
<id>pro</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<env>pro</env>
</properties>
</profile>
</profiles>
<!-- ################################ 开发,测试,生产环境配置 End #####################-->
测试库的配置,最简配置即可,单测默认不会使用数据库连接池等复杂配置
### mysql datasource config ###
spring.datasource.url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/databasename?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
封装数据层单测基类
@MybatisTest
@MapperScan("com.jd.eco.market.mysql.mapper")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public abstract class DatabaseAbstractTest extends Specification {
}
@Slf4j
public class StrategyMapperTest extends DatabaseAbstractTest {
@Resource
private StrategyMapper strategyMapper;
@Test
void selectStrategyByPoolId() {
List<StrategyDO> strategyDOList = strategyMapper.selectStrategyByPoolId(1L);
assertEquals(strategyDOList.size(), 4);
}
}
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)的作用是使用自定义的数据源,而非使用自动配置的嵌入式内存数据源
@MapperScan(“com.jd.eco.market.mysql.mapper”) 用来指定mapper的位置,如果mapper使用@Mapper注解方式编码则可以忽略此注解。
三、内存数据库使用
如果我们想要保证单测可以在离线的状态下运行,同时避免污染测试数据,我们可以在单测中使用内存数据库H2。
H2内存数据库介绍
H2 是一个用 Java 开发的嵌入式数据库,它本身只是一个类库,即只有一个 jar 文件,可以直接嵌入到应用项目中。H2 主要有如下三个用途:
- 可以同应用程序打包在一起发布,这样可以非常方便地存储少量结构化数据,最常使用。
- 用于单元测试,启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态。
- 作为缓存,即当做内存数据库,作为NoSQL的一个补充。当某些场景下数据模型必须为关系型,可以拿它当Memcached使,作为后端MySQL/Oracle的一个缓冲层,缓存一些不经常变化但需要频繁访问的数据,比如字典表、权限表。
H2支持三种模式:
- 内存模式
- 嵌入式模式
- 服务器模式
我们在此就是使用H2来做单元测试,使用他的内存模式,
引入依赖
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
数据链接配置
Springboot环境可以省略这部分配置,如果不配置会有自己的一套默认配置的内存数据库,其它环境手动配置如下:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL //h2数据库的连接地址,数据库名称自定义
spring.datasource.driver-class-name=org.h2.Driver // 配置JDBC Driver
spring.datasource.username=root // 用户名,自定义
spring.datasource.password=123456 //数据库密码,自定义
/** 注:连接参数详解
DB_CLOSE_DELAY:要求最后一个正在连接的连接断开后,不要关闭数据库
MODE=MySQL:兼容模式,H2兼容多种数据库,该值可以为:DB2、Derby、HSQLDB、SQLServer、MySQL、Oracle、PostgreSQL
AUTO_RECONNECT=TRUE:连接丢失后自动重新连接
AUTO_SERVER=TRUE:启动自动混合模式,允许开启多个连接,该参数不支持在内存中运行模式
TRACE_LEVEL_SYSTEM_OUT、TRACE_LEVEL_FILE:输出跟踪日志到控制台或文件, 取值0为OFF,1为ERROR(默认值),2为INFO,3为DEBUG
SET TRACE_MAX_FILE_SIZE mb:设置跟踪日志文件的大小,默认为16M
*/
数据初始化配置
在测试过程中,一般都需要对数据库进行初始化操作。这些操作基本分为两类,一个是数据库的schema操作,如建表,建sequence,用户授权等等。第二个是数据库的数据初始化,一般是把一些公共的测试数据在这个时机导入。依旧是在application.properties文件中进行配置
### 每次启动程序,对数据库的表结构
spring.datasource.schema=classpath:sql/strategy_ddl.sql
### 每次启动程序,导入数据
spring.datasource.data=classpath:sql/strategy_data.sql
开发单元测试的过程中,sql配置文件放在指定目录下,例如:
src/test/resources/sql/strategy_ddl.sql,
src/test/resources/sql/strategy_data.sql
sql配置文件内容如下:
// 注意:建表语句后面不能有ENGINE=InnoDB DEFAULT CHARSET=utf8,不然将会报错
CREATE TABLE promotion_timeline (
id bigint(20) NOT NULL COMMENT '主键id',
promo_festival int(11) DEFAULT NULL COMMENT '促销节日编码',
promo_stage int(11) DEFAULT NULL COMMENT '促销阶段',
timeline_name varchar(32) DEFAULT NULL COMMENT '时间轴名称',
begin_time datetime DEFAULT NULL COMMENT '开始时间',
end_time datetime DEFAULT NULL COMMENT '结束时间',
remark varchar(256) DEFAULT NULL COMMENT '备注说明',
is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
create_user varchar(32) NOT NULL COMMENT '创建人',
created datetime NOT NULL COMMENT '创建时间',
update_user varchar(32) NOT NULL COMMENT '更新人',
modified datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (id)
);
INSERT INTO promotion_timeline (id, promo_festival, promo_stage, timeline_name, begin_time, end_time, remark, is_delete, create_user, created, update_user, modified)
VALUES
(201, 20200618, 1, '预热期', '2020-05-21 00:00:00', '2020-05-31 23:59:59', NULL, 0, 'system', '2020-05-14 11:43:15', 'system', '2020-05-14 11:43:15'),
(202, 20200618, 2, '专场期', '2020-06-01 00:00:00', '2020-06-15 23:59:59', NULL, 0, 'system', '2020-05-14 11:43:15', 'system', '2020-05-14 11:43:15'),
(203, 20200618, 3, '高潮期', '2020-06-16 00:00:00', '2020-06-18 23:59:59', NULL, 0, 'system', '2020-05-14 11:43:15', 'system', '2020-05-14 11:43:15'),
(204, 20200618, 4, '返场期', '2020-06-19 00:00:00', '2020-06-21 23:59:59', NULL, 0, 'system', '2020-05-14 11:43:15', 'system', '2020-05-14 11:43:15'),
(401, 20200601, NULL, '新品营销中心', '2020-06-01 00:00:00', '2099-01-01 00:00:00', NULL, 0, 'system', '2020-06-11 15:04:45', 'system', '2020-06-11 15:04:45'),
(601, 20200701, NULL, '日常策略', '2020-06-25 00:00:00', '2099-01-01 00:00:00', NULL, 0, 'system', '2020-06-27 18:03:36', 'system', '2020-06-27 18:03:36'),
(1316717177201463298, 20200702, 1, '线上验证-wl', '2020-10-15 00:00:00', '2020-10-30 23:59:59', NULL, 1, 'wangling113', '2020-10-15 20:26:53', 'wangling113', '2020-10-15 20:27:47'),
(1316736623085326338, 20200702, 1, '预售期', '2020-10-21 00:00:00', '2020-10-31 23:59:59', NULL, 0, 'jiangjian45', '2020-10-15 21:44:09', 'zhangqiu8', '2020-11-13 14:29:45'),
(1316736623114686466, 20200702, 2, '专场期', '2020-11-01 00:00:00', '2020-11-08 23:59:59', NULL, 0, 'jiangjian45', '2020-10-15 21:44:09', 'zhangqiu8', '2020-11-13 14:29:45'),
(1316736623165018113, 20200702, 3, '高潮期', '2020-11-09 00:00:00', '2020-11-11 23:59:59', NULL, 0, 'jiangjian45', '2020-10-15 21:44:09', 'zhangqiu8', '2020-11-13 14:29:45');
单元测试编写
使用内存数据库我们只需删除这个注解@AutoConfigureTestDatabase或者不指定"replace = AutoConfigureTestDatabase.Replace.NONE"即可。
@MybatisTest
@MapperScan("com.jd.eco.market.mysql.mapper")
@AutoConfigureTestDatabase
public abstract class DatabaseAbstractTest extends Specification {
}
public class PromotionTimelineMapperTest extends DatabaseAbstractTest {
@Resource
private PromotionTimelineMapper promotionTimelineMapper;
// @Sql({ "classpath:sql/strategy_data.sql" }) 如果单测需要特定数据可以通过这种方式加载
@Test
void selectBaseByFestival() {
List<PromotionTimelineDO> timelineDOList = promotionTimelineMapper.selectBaseByFestival(20200618);
assertEquals(timelineDOList.size(), 4);
}
}
如下图所示,启动单测时加载了h2内存数据库