引子
UT的重要性不言而喻,这里不用多说。但是,码农都知道,一段逻辑往往涉及到很多外部系统调用(不同的数据源、不同的服务等等),配合完成一段code真正想要完成的逻辑。
而UT(Unit Test)本身的重要思想之一,就是测试本单元的核心逻辑。我测试A,你却因为A依赖B,而导致测试的code跑不了,这个不科学。
于是,有了各种各样的mock技术,来模拟B的行为,按照你的需要,返回你期望的数据。这种测试方式,很好的实现了自身业务逻辑接触对外部系统的依赖,在Service层面非常有意义。
但是,在对DAL层面的测试上,Mock技术却不太合适。原因很明显,DAL层面上,通常没有负责的业务逻辑,code本身更多的是CRUD的操作,测试本身更多的是关注如下几点:
- SQL本身是否有语法错误
- SQL本身的语义是否正确,比如:SQL是否按照我预设的查询条件返回了正确的结果。
- SQL执行过程中,原生的结果集和使用的ORM框架有映射关系的配置错误。
鉴于以上特点,你会发现,对于这层的测试,我们就是希望SQL真实的去跑。Mock掉这部分的代码执行逻辑,测试本身的意义也就不大了。但是,真的执行SQL,不就又使UT跟DB这个外部环境挂上勾了吗?这个不是特别蛋疼。。。
针对这种情况,码农常见的处理手法有两种:
- 找一个可以运行的测试环境的DB实例,连接上去,真跑一下。这个实例可能是测试环境本身的,也可能是开发同学自己本地启动的。总之,这时代码的执行,还是依赖了一个外部系统。
- 爷特么不测了。直接测试环境或者预发布环境跑起来看。。。额,对于这种,下面的文字就基本没意义了。。。
本文主要的目的,是从实战的角度,介绍一个可行的方案去避免上面提到的问题,同时达到测试的目的。
做好DAL层UT需要解决的问题
既然要稍微优雅点的完成上面的任务,那么,必须解决如下的几个问题:
也是本文后面会介绍的几个使用工具。
- 如何隔离对外部DB的依赖:HSQLDB是一个纯java实现轻量级DB,其中提供的内存运行模式,非常适合UT这种场景。
- 如果内存DB随着UT启动,那么何时以及如何建立我需要的DB Table呢:Apache DdlUtils是个轻量级的DDL Java工具,非常适合结合HSQLDB一块使用,完成对DB table schema的执行创建过程。
- 如何初始化你需要的测试数据呢:DbUnit是个基于JUnit扩展出来的专门focus在帮你准备测试数据集的小项目,可以节省你很多通过JDBC方式准备数据的时间。
以上工具,其实都是单独项目,完全可以单独使用,不过组合一块,也威力无穷,实在是居家旅行、DB测试必备之利器。
基于Spring和ibatis的DAL层测试方案
上面蛋扯完了,该来点干货了。鉴于阿里系内部的DAL层绝大部分使用spring+ibatis实现,以下实例会基于这些框架的基础进行。
依赖引入
首先,在你的工程pom.xml中加入如下test scope的配置,用于测试期间的类库准备。
如下版本号,仅供参考,可以自行选择。
<!--Test scope--> <!--Used to set up a memory DB--> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> <!--Used to execute the DDL to create the table schema to create the target table in the hsqldb--> <dependency> <groupId>org.apache.ddlutils</groupId> <artifactId>ddlutils</artifactId> <version>1.0</version> <scope>test</scope> </dependency> <!--Used to prepare your test data on your table--> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.5.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8</version> <scope>test</scope> </dependency>
准备测试datasource
基于Spring的配置文件中,真实的数据源无非是datasource的配置,所以,测试前我们只要将datasource替换成我们准备的hsqldb即可。这里,准备一份test-datasource.xml,放到test/resources路径下的任何子目录中(后面的TestCase读的到即可),代码如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean name="test-dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <!--use memory mode to perform the test--> <property name="url" value="jdbc:hsqldb:mem:uic-testdb"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <!-- Used for Ibatis --> <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean" abstract="true"> <property name="dataSource" ref="test-dataSource" /> </bean> </beans>
以上配置中,需要注意如下几点:
- test-dataSourcebean配置只要照抄就好,username、password均为hsqldb的默认配置,url中的格式以冒号分割,第三段的mem会告诉hsqldb启动内存DB模式,最后一节是你的DB实例的名字,你可以随便改。driverClassName可以看出,这里启用的driver是hsqldb自己的JDBC驱动。
- sqlMapClient中直接注入这个测试用的datasource就行啦。
准备建表
这里,表本身的schema完全取决于你自己的实际项目。因为我选择了使用DdlUtils来完成建表的任务,所以按照他的解析方式,准备一份他识别的建表文件,test-create-table-ddl.xml放到test/resources路径下的任何子目录中(后面的TestCase读的到即可)。以我的项目为例,文件长成下面这个样子(关于这个文件的各个元素的详细描述,可以参考这里,但大致的用法,看下面的例子基本也就懂了。)
<?xml version="1.0"?> <!DOCTYPE database SYSTEM "http://db.apache.org/torque/dtd/database.dtd"> <!--This will be used by DdlUtils to created table schema--> <database name="uic-testdb"> <table name="uic_partner_relation"> <column name="id" type="BIGINT" required="true" primaryKey="true" size="20"/> <column name="gmt_create" type="TIMESTAMP" required="true" /> <column name="gmt_modified" type="TIMESTAMP" required="true"/> <column name="account_id" type="BIGINT" size="20" required="true"/> <column name="attribute1" type="VARCHAR" size="128" required="true" /> <column name="attribute2" type="VARCHAR" size="128" required="true"/> <column name="attribute3" type="CHAR" size="1" required="true"/> <!--You can create indexes with index tag--> <index name="ind_primary_key"> <index-column name="id" /> </index> <index name="ind_pa_pa"> <index-column name="attribute1" /> <index-column name="attribute2" /> </index> </table> </database>
这里,database标签的name属性就是你上面配置的DB实例的name,保持一致就好。
准备测试数据
因为使用DbUnit,所以按照他的规范,配置好你需要的数据即可,xml的格式比较简单,每行代表一条数据,DbUnit会自动帮你完成数据初始化的过程。假设这份文件叫prepare-test-data.xml,放到test/resources路径下的任何子目录中(后面的TestCase读的到即可)。以我的项目为例,文件长成下面这个样子:
<?xml version='1.0' encoding='UTF-8'?> <dataset> <test_db_table id='1' gmt_create='2001-01-01' gmt_modified='2001-01-01' account_id='100' attribute1='facebook' attribute2="200" attribute3="Y"/> <test_db_table id='2' gmt_create='2001-01-01' gmt_modified='2001-01-01' account_id='101' attribute1='facebook' attribute2="201" attribute3="Y"/> <test_db_table id='3' gmt_create='2001-01-01' gmt_modified='2001-01-01' account_id='101' attribute1='twitter' attribute2="201" attribute3="N"/> </dataset>
万事具体,只欠TestClass啦
上面已经把所有需要的配置文件都准备好了,以我的UTClass适当简化为例,代码如下:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"/biz/ibatis/test-datasource.xml", "/biz/spring/xxx_dal.xml"}) public class MyDaoImplTest { @Autowired MyDao myDao; @Autowired DataSource testDataSource; @Before public void setUp() throws Exception { // prepare the table first Platform platform = PlatformFactory.createNewPlatformInstance(testDataSource); Database database = new DatabaseIO().read(new InputStreamReader( getClass().getResourceAsStream("/biz/ibatis/test-create-table-ddl.xml"))); platform.alterTables(database, false); // prepare test data DataSourceDatabaseTester databaseTester = new DataSourceDatabaseTester(testDataSource); databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT); databaseTester.setDataSet(readDataSet()); databaseTester.onSetup(); } private IDataSet readDataSet() throws Exception { return new FlatXmlDataSetBuilder().build(Resources.getResource("biz/ibatis/testdata/prepare-test-data.xml")); } @Test public void testListPartnerRelationFullRecords() throws Exception { // case 1 : no record for 123 in FB List<PartnerRelationFullRecord> fullRecords = myDao.listPartnerRelationFullRecords("123", "facebook"); assertTrue(CollectionUtils.isEmpty(fullRecords)); // case 2 : only one record for 200 in FB fullRecords = myDao.listPartnerRelationFullRecords("200", "facebook"); assertEquals(1, fullRecords.size()); // case 3 : 4 records for 201 in vk fullRecords = myDao.listPartnerRelationFullRecords("201", "vk"); assertEquals(4, fullRecords.size()); } }
大概解释一下上面的code:
- 使用SpringJUnit4ClassRunner作为TestRunner运行这个TestCase,直接帮你完成依赖注入,充分利用Spring IOC的优势。
- 直接通过@ContextConfiguration注解,标识出你需要准备Spring容器。这里,第一份配置是上面介绍过的,你精心准备的测试环境的datasource的配置,第二份文件是生产直接使用的Dao的配置,代码不贴了,就是code中MyDao的配置,这就完成了测试datasource的注入。
- 参考setUp方法中的实现和注释,你基本就可以看出来如果通过DdlUtils和DbUnit完成table的建立以及测试数据的初始化。
完成了以上几步配置,你就可以开始你的逻辑测试了。不依赖网络,不依赖DB,运行全靠你自己了。
总结
总算写完了,如果你能看到这里,也算我没白码这些字。
老实说,整个过程弄下来也并不算简单,但是UT的编写是一个一劳永逸的过程,我想大家提高代码测试的意识才是最重要的。
毕竟,意识最重要,工具只是实现的手段而已。