现实系统中通常会有一些具有外部依赖性的对象这些对象和数据库或者其他对象存在诸多关联如果我们对这样的对象编写单元和组件级测试的话可以想象将是非常麻烦的一件事因为这种外部依赖性的存在使的我们很难将对象孤立出来进行测试经常提及的白盒测试法基本上就是通过控制对象的外部依赖性来达到隔离对象的目的使的可以操作这些对象的状态和相关行为
运用 模拟对象(mock objects)
或者stubs就是一个控制对象外部依赖性的解决方案通过隔离那些关联的数据库访问类象JDBC的相关操作类对于控制对象外部依赖性将是很有效的但模拟对象的解决方案对一些特殊的应用系统架构就显得力不从心了象那些运用了EJB的CMP(containermanaged
persistence)或者 JDO(java Data
Objects)的应用系统架构在这些架构里数据库的访问对象是在最底层的而且很隐蔽
由Manuel Laflamme
编写的开放源代码的DBUnit架构体系对于控制系统内部的数据库依赖性提供了一个非常不错的解决方案他允许程序员在整个的测试过程中自由的管理控制数据库的状态这很重要利用DBUnit在测试之前我们可以给目标数据库植入我们需要的数据集而且在测试完毕后数据库完全能够回溯到测试前的状态
在很多成功的软件项目中测试自动化往往是关键的层面DBUnit允许开发人员创建测试用例代码在这些测试用例的生命周期内我们可以很好的控制数据库的状态而且这些测试用例是很容易实现自动化的这样在测试过程中我们无须对它进行人工的干预为人工干预造成的后果而担心就更没必要了
简单介绍
配置使用DBUnit的第一步我们首先需要知道如何生成数据库schema这个文件是XML格式的其中包括了数据库的表及相关数据信息
例如这里有一个数据库表employee
我们可以用SQL的形式这样将他表示出来
而且我们可以看到一个简单的数据集可以这样表示
在DBUnit中上面这个表和抽样数据信息可以用XML文件的形式这样表示
<EMPLOYEE employee_uid=
start_date=
first_name=Andrew
ssn=xxxxxxxxx
last_name=Glover />
这个生成的XML格式的文件可以作为系统所需的所有种子文件(seed
files)的样本模版
为相互关联的测试场景创建多个种子文件是一个很有效的策略就象通过不同的数据库文件来区分隔离数据库状态是一个道理多种子文件策略可以将我们的测试目标锁定到较小的范围目标数据可以只针对数据库的表而不是整个数据库
为了给目标数据库植入不同的职员记录我们需要的XML数据文件如下所示
<?xml version= encoding=UTF?>
<dataset>
<EMPLOYEE employee_uid=
start_date=
first_name=Drew ssn=
last_name=Smith />
<EMPLOYEE employee_uid=
start_date=
first_name=Nick ssn=
last_name=Marquiss />
<EMPLOYEE employee_uid=
start_date=
first_name=Jose ssn=
last_name=Whitson />
</dataset>
现在要让DBUnit和我们所需的数据库schema一起工作了对于程序员来说我们使用DBUnit进行测试可以有两种选择通过直接编码方式进行测试或者与Ant结合
编码方式
DBUnit框架提供了一个基本的抽象测试用例类叫做DatabaseTestCase它是JUnit框架中的基础类TestCase的子类如果我们使用这个类必须首先实现两个钩子方法(hook
methods)getConnection()和getDataSet()
方法getConnection()需要返回一个IDatabaseConnection类型的对象这个对象是一个基于普通JDBC连接的包装类例如下面的代码段演示了在MySQL数据库环境下IDatabaseConnection类型连接对象的创建方法
protected IDatabaseConnection getConnection()
throws Exception {
Class driverClass = ClassforName(orggjtmmmysqlDriver);
Connection jdbcConnection = DriverManagergetConnection(
jdbc:mysql:///hr hr hr);
return new DatabaseConnection(jdbcConnection);
}
方法getDataSet()返回一个IDataSet类型对象其实说白了他就是我们先前提到的XML数据的种子文件的另一种表现形式
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(
new
FileInputStream(hrseedxml));
}
有了这两个基本的方法以后DBUnit就可以按照它预先缺省的行为工作了DatabaseTestCase类提供了两个fixture(我叫它固件不知仁兄同意否?)方法来控制测试前和测试后的数据库状态这两个方法就是
getSetUpOperation() 和 getTearDownOperation()
一种高效的实施方案就是让getSetUpOperation()方法执行REFRESH操作通过这个操作我们可以用种子文件中的数据去更新目标数据库里的数据接下来就是getTearDownOperation()让他去执行一个NONE操作也就是什么也不执行
protected DatabaseOperation getSetUpOperation()
throws
Exception {
return DatabaseOperationREFRESH;
}
protected DatabaseOperation getTearDownOperation()
throws
Exception {
return DatabaseOperationNONE;
}
还有一种有效的方法就是在getSetUpOperation()方法中执行CLEAN_INSERT操作这样首先会将目标数据库中与我们提供的种子文件一致的数据删除然后将我们提供的数据插入到数据库中这个实施顺序保证了我们对数据库的精确控制
代码样例
在一个基于JEE的人力资源系统中我们很希望对某个数据操作周期实现测试自动化这个操作周期包括职员的新增检索更新和删除远程接口定义了下列的业务方法(为了简洁清楚省略了方法中的throws子句)
//译者注这里的EmployeeValueObject类型对象译者认为是代表职员实体信息的对象
public void createEmployee( EmployeeValueObject emplVo )
public EmployeeValueObject getEmployeeBySocialSecNum( String ssn )
public voidupdateEmployee( EmployeeValueObject emplVo )
public voiddeleteEmployee( EmployeeValueObject emplVo )
测试getEmployeeBySocialSecNum()方法
需要植入一条数据到目标数据库中另外测试deleteEmployee()方法和updateEmployee()方法时同样也是在先前植入的这条记录的基础上进行最后测试类会首先利用createEmployee()方法创建一条记录同时我们需要校验执行这个方法时是否会有异常发生
下面这个DBUnit种子文件叫做employee_hr_seedxml下面将用到这个文件
<?xml version= encoding=UTF?>
<dataset>
<EMPLOYEE employee_uid=
start_date=
first_name=Drew ssn=
last_name=Smith />
<EMPLOYEE employee_uid=
start_date=
first_name=Nick ssn=
last_name=Marquiss />
<EMPLOYEE employee_uid=
start_date=
first_name=Jose ssn=
last_name=Whitson />
</dataset>
测试类 EmployeeSessionFacadeTest
需要扩展DBUnit的基础类DatabaseTestCase并且必须提供对getConnection()和getDataSet()方法的实现在getConnection()方法中将获得与EJB容器初始化时一样的数据库实例getDataSet()方法负责读取上面提及的employee_hr_seedxml文件的数据
测试方法相当简单因为DBUnit已经为我们处理了复杂的数据库生命周期任务为了测试getEmployeeBySocialSecNum()方法只需要简单的传递一个存在于种子文件中的社保代码号即可比如
//译者注EmployeeFacade 类型对象译者认为是代表底层数据库数据的映射体
public void testFindBySSN() throws Exception{
EmployeeFacade facade = //obtain somehow
EmployeeValueObject vo =
facadegetEmployeeBySocialSecNum();
TestCaseassertNotNull(vo shouldnt be null vo);
TestCaseassertEquals(should be Drew
Drew vogetFirstName());
TestCaseassertEquals(should be Smith
Smith vogetLastName());
}
为了确保操作周期中的创建职员方法createEmployee()没有问题我们只需简单的执行一下这个方法然后校验一下看有没有异常抛出另外下一步我们要做的就是在这条新增的记录上进行查找操作看是否可以找到刚创建的记录
public void testEmployeeCreate() throws Exception{