单元测试引入hsqldb探索

31 篇文章 1 订阅
2 篇文章 0 订阅

为了提高代码和产品质量,我们网站的大部分产品都接入了单测平台,任何任务在提测前都要执行整个提测工程的单元测试,测试结果全部通过并且覆盖率统计等指标达标之后才能通过提测申请。这当然对于提高产品代码质量是有非常大的好处的,有助于识别代码中的bug。但是在执行的过程中可不是那么顺利的,主要体现在有时单测执行失败并不是代码有什么bug,而是单测依赖的数据发生了变化导致单测的执行结果和预期的结果不一样assert失败,或者单测中依赖了其它模块的接口,下游环境出现了异常导致了单测执行失败。事实上在我们的开发任务中消耗了大量的时间来解决这两方面的单测问题,有时候可能编写代码只用了一个小时,但是折腾单测却会花掉一天,效率低下。对于第二种情况主要的做法是在单测中把依赖的远程接口mock掉,有一些开源的mock框架如Mockito、EasyMock可以使用,这里不多展开。这里主要探索一下第一种情况即单测依赖的数据不稳定造成单测结果不稳定。

平台每天最少要跑几十次单测,所以跑完单测之后最好要清理现场还原数据,否则测试库每天会多出很多垃圾数据。目前大多数做法都是按照下列步骤:
1.单测开始前准备数据插入数据到测试库;
2.执行单测逻辑;
3.还原现场,数据回滚。

这种做法大部分情况下是非常OK的,但是有些情况还是会造成问题:
1.虽然单测前构造了自己的测试数据,但是不能完成排除掉测试数据变化对测试结果造成的影响,比如有个case是要查询用户的最近的10个订单,或者是查询用户总共的订单数量,一旦有人用相同的测试账号新增了订单,那么就可能会导致测试case结果预测失败,assert报错。
2.单测结束后要回滚数据,要达到这个目的就要把测试代码嵌入到事务上下文中,这种做法实际上是变相的修改了执行逻辑,如果使用了mybatis框架,由于事务缓存的作用在某些情况下会产生问题,比如使用了oracle的sequence,在代码中执行了超过一次的sequence的nextVal,如果单测套上了事务,那么执行这段逻辑的时候后面不管查多少次nextVal,查出来的值跟第一次的值是一样的,因为后面几次查询都被缓存拦住了,这样会出现一些奇怪的问题。

hsqldb是一款纯Java编写的免费数据库,并且支持Memory-Only模型,所以每个单测使用独立的数据库就有了可能,Memory-Only模型数据不会持久化跑完即销毁,所以理论上可以解决上面两个问题。
因为我们正式的业务使用的都是mysql或者oracle数据库,要在单测中引入hsqldb,那么必然要让单测执行过程中读写指向到hsql数据库,这就涉及到db替换的过程。首要有一点很重要的是这个替换DB的过程不能侵入到业务代码中,不能因为单测的便利而给业务代码带来额外的风险。好在在maven工程中,单测代码和resource都可以跟业务代码隔离。我们可以定义一个额外的单测入口配置,并且利用spring容器中“同名的bean后注册的覆盖先注册”的这个规则来替换DB数据源。
先定义个单测入口配置文件ut_entry.xml,然后在单测class中import这个配置。

@ContextConfiguration({ "classpath:ut_entry.xml" })
public class BaseTestCase extends AbstractTestNGSpringContextTests {

//......
}

在配置中嵌入hsqldb数据库:

<jdbc:embedded-database id="rdsMockDataSource">
     <jdbc:script location="classpath:db_mock/rds/rds_hsql.script"/>
 </jdbc:embedded-database>

embedded-database支持几种type:HSQL、H2、DERBY,默认type是HSQL。
在rds_hsql.script添加需要创建的表等DDL SQL,这些SQL会在HSQL数据库初始化时执行:

CREATE TABLE tb_authenticate_other (
  id bigint NOT NULL AUTO_INCREMENT,
  auth_Id varchar(60) NOT NULL,
  account_id varchar(60) NOT NULL,
  id_card_name varchar(516) NOT NULL,
  id_card_num_enc varchar(1024) NOT NULL,
  id_card_num_md5 varchar(64) NOT NULL,
  id_card_photo_enc varchar(1024) DEFAULT NULL,
  user_id bigint NOT NULL,
  create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
) ;

create unique INDEX uk_auth_Id on tb_authenticate_other(auth_Id);
create INDEX uk_account_num_Id on tb_authenticate_other (account_id,id_card_num_md5);

上面是用HSQL来替换MySQL数据库,可以变相的支持MySQL中HSQL不支持的语法,比如HSQL不支持MySQL的on update语法,可以通过触发器来变相的支持,增加下面的脚本:

CREATE TRIGGER trig_authenticate_other AFTER UPDATE ON tb_authenticate_other
  referencing NEW ROW AS newrow
  FOR EACH ROW WHEN (newrow.update_time IS NULL)
  UPDATE tb_authenticate_other SET update_time = CURRENT_TIMESTAMP;

如果使用了一些HSQL中不支持的MySQL函数,也可以在该脚本中处理,自定义一个一模一样的函数,比如你需要一个日志格式函数:

create function DATE_FORMAT(t TIMESTAMP, pattern VARCHAR(60)) returns TIMESTAMP
  return t;

然后定义一个和业务配置同名的sqlSessionFactory和TransactionManager,把hsqldb数据源注入到新定义的sqlSessionFactory和TransactionManager中:

<bean id="rdsSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="configLocation" value="classpath:mybatis-config-rds.xml"/>
     <property name="dataSource" ref="rdsMockDataSource"/>
 </bean>

 <bean id="rdsTransactionManager"
       class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="rdsMockDataSource"/>
     <property name="nestedTransactionAllowed" value="true"/>
 </bean>
 <tx:annotation-driven transaction-manager="rdsTransactionManager"/>

Spring容易运行在不同的xml中定义同名的bean,并且后加载的bean会覆盖先加载的bean,所以就达到了运行单测时替换数据源的目的。

如果你的应用中使用了oracle数据库,那么可以按照这种步骤再定义一个mock oracle数据库的HSQL数据源。使用步骤还是很简单的,接下来就可以愉快的写单测代码并且愉快的跑单测了。这里再安利一个自动生成数据的框架podam,爱偷懒的可以去百度尝试一下。

hsqldb使用方便,内存模式执行效率高,能够提高单测的稳定性。但是使用的过程中还是发现一些问题:
1.mysql中使用了CURRENT_TIMESTAMP()的sql用hsqldb执行会报错,自定义function也不行,因为CURRENT_TIMESTAMP在hsqldb中也是个关键字。由于mysql中CURRENT_TIMESTAMP和CURRENT_TIMESTAMP()效果是一样的,所以可以把业务代码中的sql改成CURRENT_TIMESTAMP解决这个问题。
2.hsqldb的数据类型和mysql、oracle有差异,比如没有oracle中的number类型,tinyint、timestamp等类型不能指定长度等。
3.不支持oracle的connect by语法。
4.merge语法的update分支不能有where语句,而oracle是支持的。
5.在处理mybatis的keyColumn功能时,字段名称是大小写敏感的且只能用大写的,比如下面这样的,只能把id都改成大写的ID,这应该是hsqldb客户端的bug。

<insert id="xxx" parameterType="xxx" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
</insert>

总体来说,跟mysql的兼容性要好一些跟oracle的兼容性差一些,项目中是否使用hsqldb要结合它带来的收益和付出的代价做综合考量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值