如何优雅的去做DAL层的UT

引子

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这个外部环境挂上勾了吗?这个不是特别蛋疼。。。

针对这种情况,码农常见的处理手法有两种:

  1. 找一个可以运行的测试环境的DB实例,连接上去,真跑一下。这个实例可能是测试环境本身的,也可能是开发同学自己本地启动的。总之,这时代码的执行,还是依赖了一个外部系统。
  2. 爷特么不测了。直接测试环境或者预发布环境跑起来看。。。额,对于这种,下面的文字就基本没意义了。。。

本文主要的目的,是从实战的角度,介绍一个可行的方案去避免上面提到的问题,同时达到测试的目的。

做好DAL层UT需要解决的问题

既然要稍微优雅点的完成上面的任务,那么,必须解决如下的几个问题:
也是本文后面会介绍的几个使用工具。

  1. 如何隔离对外部DB的依赖:HSQLDB是一个纯java实现轻量级DB,其中提供的内存运行模式,非常适合UT这种场景。
  2. 如果内存DB随着UT启动,那么何时以及如何建立我需要的DB Table呢:Apache DdlUtils是个轻量级的DDL Java工具,非常适合结合HSQLDB一块使用,完成对DB table schema的执行创建过程。
  3. 如何初始化你需要的测试数据呢:DbUnit是个基于JUnit扩展出来的专门focus在帮你准备测试数据集的小项目,可以节省你很多通过JDBC方式准备数据的时间。

以上工具,其实都是单独项目,完全可以单独使用,不过组合一块,也威力无穷,实在是居家旅行、DB测试必备之利器。

基于Spring和ibatis的DAL层测试方案

上面蛋扯完了,该来点干货了。鉴于阿里系内部的DAL层绝大部分使用spring+ibatis实现,以下实例会基于这些框架的基础进行。

扫描二维码关注公众号,回复: 1179818 查看本文章

依赖引入

首先,在你的工程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的编写是一个一劳永逸的过程,我想大家提高代码测试的意识才是最重要的。
毕竟,意识最重要,工具只是实现的手段而已。

猜你喜欢

转载自hittyt.iteye.com/blog/2122829
DAL
今日推荐