likes
comments
collection
share

单测写得好,年终少不了———这样写单测很牛 X,老板都得跟你学

作者站长头像
站长
· 阅读数 8

你是否有这样的窘境,在执行一次单元测试后,再次执行却会失败。这是因为上一次的单元测试修改了数据,导致下一次执行时业务逻辑不同,必须重新构建数据才能再次执行单元测试。每写一次单元测试只能使用一次,耗费时间与精力,久而久之,我们就不愿意维护单元测试了。

这篇文章将告诉你如何优雅的解决这类问题。

单元测试不应该依赖真实环境的数据库,而应该使用嵌入式数据库。通过使用嵌入式数据库,每次测试创建或修改的数据都不会对真实环境造成污染,数据将被写入全新的数据库,从而保证每次单元测试执行前的条件完全一致。(如果使用测试环境的真实数据库,两次测试执行之间数据库可能已经发生了变化,导致测试结果不一致)

嵌入式中间件 提供了非常理想的隔离环境。保证单测的结果不会被历史数据影响。

H2 内存数据库介绍

H2 数据库是一个用 Java 开发的嵌入式(内存级别)数据库,它本身只是一个类库,也就是只有一个 jar 文件,可以直接嵌入到项目中。H2数据库又被称为内存数据库,因为它支持在内存中创建数据库和表。所以如果我们使用H2数据库的内存模式,那么我们创建的数据库和表都只是保存在内存中,一旦应用重启,那么内存中的数据库和表就不存在了。

H2数据库由于只是一个jar包,和Java应用运行在jvm中,所以进程终止,数据就没了,下次再启动,还是全新的环境。这些特性非常适合应用于单元测试。

引入H2,单测可以不再依赖测试环境数据库,每次单测执行也都不会修改测试数据库。所以单测可以重复执行。方便调试代码和自动化回归测试。(调用下游的RPC如果是写接口依然需要mock!!!)

接入方式

pom 依赖

pom依赖等级是test,完全不会影响到正式代码。无须担心线上环境的风险

<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

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数据库。这种方法的好处是,无需修改正式代码,也无需添加额外的配置。

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如下。

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 并不完全一致,需要注意下

  1. COLLATE utf8mb4_unicode_ci 这类关键词要去掉。
  2. 注释:不支持表级别的Comment
  3. 索引:H2中的索引是数据库内唯一,MySQL中的索引是每张表唯一
  4. CURRENT_TIMESTAMP:H2不支持记录更新时自动刷新字段时间,也就是不支持语句ON UPDATE CURRENT_TIMESTAMP
  5. JSON:H2不兼容MySqlJSON字段类型,建议换成longtext
  6. 双引号转义:H2不兼容双引号转义",直接写"即可(在插入JSON 字符串时注意) 。也使用线上工具,可以去掉多余的转义 www.lzltool.com/Escape/Stri…

在单测启动类添加 BeanPostProcessor

上述内容中,我们已经编写好了 BeanPostProcessor,接下来的一步是将它注入到Spring中。可以使用单元测试注解来实现将该类注入到Spring中。

例如,在单元测试启动基类中添加如下注解:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {MyBeanPostProcessor.class})
@SpringBootTest(classes = XXXAppStarter.class,webEnvironment =
        SpringBootTest.WebEnvironment.NONE)
public abstract class BaseTest {

}

这样单测环境启动项目时,测试环境数据库就会被替换为 H2 数据库了。

快去试试吧!如果有问题,可以在文章下评论留言。

聊点其他的事情。

最近我找了出版社的编辑和几位大佬聊了一件事————出本关于电商业务架构的书,分享一下 商品、营销、订单、会员、支付各细分业务系统的建设经验。最后因法律风险这个计划将长期搁置。为什么出本书会涉及法律问题呢?

因为业务系统架构不可避免的要说明实际的业务背景和痛点,这一定会涉及公司的商业机密。即使做了大量脱敏工作,也无法保证公司不找茬。这也很容易理解,你凭什么把公司的商业机密和业务背景作为代价,去宣传、出书和赚钱呢?

换句话说现有的出版物中很少涉及业务背景和业务逻辑,这类被阉割的内容千篇一律大同小异。之所以你看完了,觉得没学到啥,因为本来就没啥干货。真正有价值的内容,大家不敢在书里发布。