你是否有这样的窘境,在执行一次单元测试后,再次执行却会失败。这是因为上一次的单元测试修改了数据,导致下一次执行时业务逻辑不同,必须重新构建数据才能再次执行单元测试。每写一次单元测试只能使用一次,耗费时间与精力,久而久之,我们就不愿意维护单元测试了。
这篇文章将告诉你如何优雅的解决这类问题。
单元测试不应该依赖真实环境的数据库,而应该使用嵌入式数据库。通过使用嵌入式数据库,每次测试创建或修改的数据都不会对真实环境造成污染,数据将被写入全新的数据库,从而保证每次单元测试执行前的条件完全一致。(如果使用测试环境的真实数据库,两次测试执行之间数据库可能已经发生了变化,导致测试结果不一致)
嵌入式中间件 提供了非常理想的隔离环境。保证单测的结果不会被历史数据影响。
H2 内存数据库介绍
H2 数据库是一个用 Java 开发的嵌入式(内存级别)数据库,它本身只是一个类库,也就是只有一个 jar 文件,可以直接嵌入到项目中。H2数据库又被称为内存数据库,因为它支持在内存中创建数据库和表。所以如果我们使用H2数据库的内存模式,那么我们创建的数据库和表都只是保存在内存中,一旦应用重启,那么内存中的数据库和表就不存在了。
H2数据库由于只是一个jar包,和Java应用运行在jvm中,所以进程终止,数据就没了,下次再启动,还是全新的环境。这些特性非常适合应用于单元测试。
引入H2,单测可以不再依赖测试环境数据库,每次单测执行也都不会修改测试数据库。所以单测可以重复执行。方便调试代码和自动化回归测试。(调用下游的RPC如果是写接口依然需要mock!!!)
接入方式
pom 依赖
pom依赖等级是test,完全不会影响到正式代码。无须担心线上环境的风险
xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
替换数据源
单元测试执行时,如果没有特别修改数据源,默认会使用测试环境数据库。为了使用H2数据库,需要在单测执行时替换原有的数据源。
在 Java 中,数据源都是java.sql.DataSource的实现类。访问数据库都是基于DataSource接口,因此只需要将原来的DataSource对象替换为H2 DataSource对象即可。
一般情况下,项目都是基于Spring框架开发,数据源DataSource会由Spring进行托管,通过Spring获取DataSource的bean进行访问。所以问题可以简化为,在项目启动时将原DataSource bean替换为H2 DataSource bean。
使用 BeanPostProcessor
Java
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Spring提供了该接口,允许开发者在Bean实例化完成后对bean进行特殊代理。postProcessAfterInitialization
方法是有返回值的,如果不需要对bean进行特殊处理,则直接返回原始的bean,如果需要特殊处理,则返回新的bean。
在下面的代码示例中,我展示了如何创建一个新的H2数据源,并在postProcessAfterInitialization方法中将原始数据源替换为H2数据源。这样系统就不会再访问测试数据库,而是使用新的H2数据库。这种方法的好处是,无需修改正式代码,也无需添加额外的配置。
Java
public static class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("testDataSource")) {//xml 中配置的数据源 Bean 名称
return initDataSource();
}
return bean;
}
}
public static DataSource initDataSource() {
EmbeddedDatabaseBuilder builder = (new EmbeddedDatabaseBuilder())
.setType(EmbeddedDatabaseType.H2)
.setName("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;mode=MySQL;TRACE_LEVEL_SYSTEM_OUT=2") //不用改动
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true);
builder.addScripts("h2_db.schema", "h2_db.data"); // 在resources 目录下 创建Sql脚本
DataSource dataSource = builder.build();
return dataSource;
}
配置数据库 schema 文件
在创建 H2 数据库时,需要执行包含创建数据库的 SQL 脚本。下面我给出一个简单的例子,大家可以根据实际需求拷贝测试环境中创建表的语句。
h2_db.schema如下。
sql
SET MODE MYSQL;
CREATE TABLE `order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
`phone` varchar(50) NOT NULL DEFAULT '' COMMENT '用户手机号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
值得注意的是 H2 数据库的关键字和 MySQL 并不完全一致,需要注意下
- COLLATE utf8mb4_unicode_ci 这类关键词要去掉。
- 注释:不支持表级别的Comment
- 索引:H2中的索引是数据库内唯一,MySQL中的索引是每张表唯一
- CURRENT_TIMESTAMP:H2不支持记录更新时自动刷新字段时间,也就是不支持语句ON UPDATE CURRENT_TIMESTAMP
- JSON:H2不兼容MySqlJSON字段类型,建议换成longtext
- 双引号转义:H2不兼容双引号转义",直接写"即可(在插入JSON 字符串时注意) 。也使用线上工具,可以去掉多余的转义 www.lzltool.com/Escape/Stri...
在单测启动类添加 BeanPostProcessor
上述内容中,我们已经编写好了 BeanPostProcessor,接下来的一步是将它注入到Spring中。可以使用单元测试注解来实现将该类注入到Spring中。
例如,在单元测试启动基类中添加如下注解:
Java
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {MyBeanPostProcessor.class})
@SpringBootTest(classes = XXXAppStarter.class,webEnvironment =
SpringBootTest.WebEnvironment.NONE)
public abstract class BaseTest {
}
这样单测环境启动项目时,测试环境数据库就会被替换为 H2 数据库了。
快去试试吧!如果有问题,可以在文章下评论留言。
聊点其他的事情。
最近我找了出版社的编辑和几位大佬聊了一件事------------出本关于电商业务架构的书,分享一下 商品、营销、订单、会员、支付各细分业务系统的建设经验。最后因法律风险这个计划将长期搁置。为什么出本书会涉及法律问题呢?
因为业务系统架构不可避免的要说明实际的业务背景和痛点,这一定会涉及公司的商业机密。即使做了大量脱敏工作,也无法保证公司不找茬。这也很容易理解,你凭什么把公司的商业机密和业务背景作为代价,去宣传、出书和赚钱呢?
换句话说现有的出版物中很少涉及业务背景和业务逻辑,这类被阉割的内容千篇一律大同小异。之所以你看完了,觉得没学到啥,因为本来就没啥干货。真正有价值的内容,大家不敢在书里发布。
所以接下来,我将继续在掘金认真分享在互联网大厂的业务系统建设经验,分享更多的干货和经验,也尽可能做到业务脱敏,不损害公司的利益。 大家可以关注我,随时看到不注水的经验分享