一般我们都会用JUnit写单元测试,但是如果测试涉及到数据库,就会有点麻烦。
总结下我的一些解决方法,当然未必是最好的。
单元测试的原则是尽量不要有外部依赖,每一个测试都是可独立运行的。“A good test set is self-sufficient and creates all the data it needs”。
如果测试中要连数据库,目前的做法一般是在配置文件中写死测试数据库的地址。如果我换了一个网络环境,比如说从办公网络切换回家里,就没办法跑测试了。对于项目开发而言这是完全可以接受的,但对于完美主义者而言就很别扭。。。我是希望单元测试能够“write once, run everywhere”。
如果测试依赖于数据库的状态,就更糟糕了。可能在某一时刻测试能通过,但谁更新了一条记录,就会fail,这种问题排查起来也很麻烦。如果测试代码本身就要修改数据库,但测试结束后没有改回来,可能会引起更多的隐形问题。理论上测试前后所有的状态应该是一致的。
综上,其实有两个问题:
- 测试依赖于具体的数据库实例。这个实例只有在特定条件下能访问(内网/localhost)。
- 测试依赖于数据库的状态,而这个状态不可控。
解决思路也有两种:
- 每次测试都搞一个“空白”的数据库。
- mock一个DAO层,将所有对数据库的操作用mock对象模拟。
Mockito
关于mock首先想到的就是Mockito,我觉得这是用着最简单的mock框架,可以用来mock各种对象,当然也可以mock DAO。
Mockito的用法就不详细说了,而且Mockito的源码可读性很好,碰到问题看源码很方便。
问题在于,如果要把所有DAO的方法都mock一次,工作量也太大了。。。而且这些代码是不能复用的。
能用配置解决的尽量不要写代码。。。
DBUnit
DBUnit是专门用来解决问题2的,用来维护测试前后数据库的状态。它的思路很简单:测试前备份数据库,准备测试需要的数据;测试后,从备份中还原数据库的状态。具体的用法不详细说了。
DBUnit的问题在于:1.不能解决多个人同时更新状态的问题。官方的最佳实践要求每个开发人员有自己测试数据库。2.没解决网络环境的问题。3.要写很多xml配置文件,很烦。。。
HSQLDB
这个要重点说说。其实刚开始碰到数据库单元测试的问题时,我的想法很简单,问题的根源就在于所有的测试共用同一个数据库实例,那就搞个嵌入式数据库好了啊,每个测试初始化自己“专用”的数据库,自己创建schema/创建测试数据。这样可以保证这个测试在任何时间、任何地方都可以执行。正好以前调研WhiteElephant知道了hsqldb,就拿来试试。
其实嵌入式数据库有很多,最出名的应该是SQLite。还有derby,用过hive的人应该都知道。还有一个叫H2的。但HSQLDB的优势在于:1.纯java写的,兼容性好;2.内存模式很强大,类似一个沙盒,很适合用于测试。
首先在maven中引入依赖:
|
|
用hsqldb创建一个内存数据库非常简单,只要写入一个特殊的jdbc url即可:
|
|
拿到了Connection对象,就可以做自己的事了,可以任意按自己的业务逻辑操作。
我是把建表语句写到一个文件里,然后在测试用例初始化时加载:
|
|
hsqldb_schema.sql例子:
|
|
这样每个测试都不会互相干扰,有自己专用的数据库,自己维护状态。感觉还是比较方便的。
Liquibase
Liquibase其实跟单元测试没啥关系,只是我觉得比较有用,顺便记录下以备忘。
liquibase的本质就是一个数据库的版本管理工具。我们在项目中,经常涉及数据表schema的变更,比如加个字段/加个索引之类的,所以数据库也是有版本的概念的。以前的管理方法是直接保存所有的sql语句到一个文件中,然后不断追加,比如:
|
|
显然这种管理方式比较麻烦,很容易出错。更关键的是没有rollback过程。如果新版本上线了有bug,要回滚到旧版本,数据库也要回滚到旧版,就只能手动操作。
liquibase就是用来解决这种问题的,它使用一个changelog文件跟踪数据库的变化,每次变化可以抽象为一个changeset。通过changelog实现对数据库schema的管理,用户可以update到任意版本,可以回滚,也可以在不同版本之间diff。changelog一般是个xml文件,但也可以直接写sql语句,很方便。详细的用法参考官方文档,这里不列出了。