构造测试桩太麻烦是项目组抱怨单元测试难做的主要原因之一,尤其是WEB应用程序开发,大量对象是由WEB容器生成,如HttpServletRequest、HttpServletResponse、ServletContext等,只有将程序布署到服务器上才能获得这些对象,这样带来的麻烦是:一方面被测对象难于孤立,输入输出难以自由控制;另一方面每次运行都要将代码布署到服务器上很浪费时间,无法脱离服务器独立运行。目前构建测试桩的首选工具是EasyMock,使用它之前需要在CLASSPATH上加上它提供的JAR包。
1) EasyMock的原理
EasyMock模拟对象的方法来自于JDK提供的对象代理机制,java.lang.reflect.Proxy类的静态方法newProxyInstance用于生成代理对象,以下是方法原型:
Object Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
其中的interfaces就是代理对象将要实现的接口,调用代理对象的方法后要执行的代码在InvocationHandler接口实现中定义。
这儿的代理对象,被EasyMock用于模拟接口对象,我们称其为Mock对象。
2) EasyMock的使用步骤
在EasyMock中,生成和使用Mock对象的步骤很简单:生成Mock对象-->录制-->回放-->验证。MockControl类是EasyMock的Façade对象,这个对象暴露了EasyMock的全部使用方法,也就是说你只需要关心这个类提供的方法即可。
u 生成Mock对象
以下代码生成了一个mock对象,类似于Java语言的new操作。但首先须获得MockControl对象,其中的Collaborator就是要模拟的接口对象。
control = MockControl.createControl(Collaborator.class);
mock = (Collaborator)control.getMock();
u 录制
获得Mock对象后默认处于录制状态,这时你可以根据你的预期指出将要调用到Mock对象的哪些方法、传给方法的参数对象是什么,进一步可以指定方法的返回值或者要求抛出一个异常。以下代码表示预期会调用到mock对象的voteForRemoval方法,传进来的参数是"Document"字符串,并且调用该方法后希望它返回值-42。
control.expectAndReturn(mock.voteForRemoval("Document"), -42);
u 回放
就是将Mock对象与被测单元关联,实施对被测单元的调用。调用到Mock对象的预期方法后就按录制时给出的方法返回值返回。以下语句表示开始回放:
control.replay();
u 验证
EasyMock能够验证的内容包括:在被测单元执行中是否按录制时列出的方法预期调用到了、传给方法的参数值是否也是预期的。只需要调用以下语句即可:
control.verify();
如果验证失败则会抛出JUnit定义的异常AssertionFailedError,表示用例执行失败。
3) “类”对象模拟
EasyMock不仅可以模拟“接口”对象,还可以模拟“类”对象,用到的主要类是MockClassControl,该类继承自MockControl,使用方法同MockControl,也就是将前面用到MockControl的地方换成MockClassControl即可。如下例:
MockControl ctrl = MockClassControl.createControl(ToMock.class);
ToMock mock = (ToMock) ctrl.getMock();
也可以只模拟部分方法,以下代码表示只模拟ToMock类的无参数方法mockedMethod:
MockControl ctrl = MockClassControl.createControl(ToMock.class, new Method[] { ToMock.class.getDeclaredMethod("mockedMethod", null) } );
4) 新版EasyMock2.0
EasyMock也有新旧版本的区别,EasyMock2.0后使用了JDK5.0的新特性泛形类(generic class)。2.0版本的使用,大的步骤同前,主要有以下特点:
u 主要使用了JDK5.0的Generic Class
u 直接使用org.easymock.EasyMock提供的静态方法,可在文件开始静态引入:
import static org.easymock.EasyMock.*;
Ø createMock方法用于直接生成Mock对象,省去了先获得MockControl对象的麻烦,如下例:
mock = createMock(Collaborator.class);
Ø replay方法和verify方法可通过参数指定多个Mock对象
replay(requestObj, contextObj, dispatcherObj);
Ø expect用于指定方法的期望返回值、调用次数、期望抛出的异常等,如下例:
expect(mock.voteForRemoval("Document")).andReturn((byte) -42);
u 验证失败抛出JDK自带的AssertionError异常
u 可在多个Mock对象间验证方法调用顺序
u 方法的参数比较规则是以方法调用形式实现的,另外还可以自定义比较器
前面已经提到,业软的很多产品访问数据库的代码量占很大比例,目前各版本开发中访问数据库的技术有很多,如JDBC、EntityBean、Hibernate、Spring、iBATIS等,不管你使用哪种技术,实际上归根结蒂都是通过Java代码访问数据库,对于这些访问数据库的代码的单元测试长期以来存在以下问题:首先,如果将数据库层用桩取代,一方面构建桩的工作量巨大,另一方面即使桩能够构建完成,其实也不太容易发现代码中的BUG,因为隔离了数据库后其实代码逻辑就比较简单了,综合考虑,我们的建议是只有真实地连数据库才能真正有效地对单元做测试;但真实地连数据库又会带来另外两个问题:
u 如何确保每个用例执行前的数据库环境是可预期的?也就是数据库的初始化问题。
u 如何确保用例执行过程中正确地操作了数据库?也就是用例执行后的数据库验证。
通过在项目组的推广试用,我们发现DBUnit很好地解决了这两个问题,它的主要功能包括测试前初始化数据库数据,测试结束后验证数据库数据,另外,DBUnit提供有自定义ANT任务,结合ANT实现前述功能。要使用DBUnit也是只需要在你的CLASSPATH路径中加上它的JAR包。
DBUnit提供有基础类DBTestCase,该类继承自JUnit的TestCase,写测试用例的方法同JUnit,下面列出我们自己写的测试用例的基本框架,余下的任务只是增加测试方法。
public class MyDBAccessTest extends DBTestCase
{
private MyDBAccess access = null;//
这是被测类,含有要测的访问数据库的方法
static
{
//
请参见说明一
System.setProperty( PropertiesBasedJdbcDatabaseTester.DBUNIT_DRIVER_CLASS, "oracle.jdbc.driver.OracleDriver");
System.setProperty( PropertiesBasedJdbcDatabaseTester.DBUNIT_CONNECTION_URL, "jdbc:oracle:thin:@10.164.22.163:1521:ise");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_USERNAME, "tzs21911");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_PASSWORD, "tzs21911");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_SCHEMA, "TZS21911");//
必须大写
}
protected void setUp() throws Exception
{
super.setUp();
access = new MyDBAccess();
}
protected void tearDown() throws Exception
{
super.tearDown();
this.getConnection().close();
access = null;
}
@Override
protected IDataSet getDataSet() throws Exception
{
//
请参见说明二
return new FlatXmlDataSet(new FileInputStream("dataset.xml"));
}
@Override
protected DatabaseOperation getSetUpOperation() throws Exception
{
//
请参见说明三
return DatabaseOperation.CLEAN_INSERT;
}
}
u 说明一
设置要连接的数据库属性,依次包括:JDBC驱动程序类、被访问的数据库的URL、数据库登录用户名、口令、数据库SCHEMA,这些参数用于创建数据库连接。这里是通过系统属性(System Property)的形式告诉DBUnit的,也是默认方式,另外的方式还有DataSource方式、JNDI方式,如果需要请参考javadoc文档。这里要注意的是ORACLE数据库下必须要提供有SCHEMA,且必须是字母大写,如果省略SCHEMA,则要将你的数据库连接权限配置成只能访问当前用户的SCHEMA。
u 说明二
这是必须要实现的模板方法(template method),目的是告诉DBUnit要初始化数据库的数据,数据的描述方式有多种,常用的称为FlatXml格式,其中的dataset.xml就是这种格式的文件名,下面是这个文件的例子:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<USERS ID='1' NAME='taozs1' AGE="31"/>
<USERS ID='2' NAME='taozs2' AGE="32"/>
<USERS ID='3' NAME='taozs3' AGE="33"/>
</dataset>
<dataset>元素间的每一行表示要初始化的一条记录,其中USERS是表名,ID、NAME、AGE是表字段,=后面表示字段值。可以在一个文件中初始化多个表,但如果表之间有约束关系(如外键),要注意表的先后顺序。
该方法返回数据集(DataSet),注意数据集可以是多个表的集合
u 说明三
这是告诉DBUnit在执行用例前(测试函数运行前,在setUp方法里被调用到)要执行的数据库初始化操作(Operation),数据的来源就是前面提到的getDataSet方法返回的数据集。有多个操作类型,常用的有:
往数据库表里插入数据集中的数据,前提条件是数据库表里没有这些数据。
删除数据库里数据集包含的记录,注意数据集里不含有的记录不删除。
删除数据库里数据集指定的表的所有记录。
DatabaseOperation.CLEAN_INSERT
先执行DELETE_ALL操作,再执行INSERT操作,这是最常用的操作。
在运行了被测方法后,我们需要验证该方法对数据库的操作是否正确,以下是DBUnit提供的类的两个静态方法,用于验证数据库实际结果与预期结果是否一致,如果不一至,会报用例执行不通过的错误。预期结果也是以FlatXml文件格式表示的。
public class Assertion
{
public static void assertEquals(ITable expected, ITable actual)
public static void assertEquals(IDataSet expected, IDataSet actual)
}
下面是一个验证的例子:
public void testMe() throws Exception
{
// 调用被测单元操作数据库
...
// 之后从数据库取回数据
IDataSet databaseDataSet = getConnection().createDataSet();
ITable actualTable = databaseDataSet.getTable("TABLE_NAME");
//从FlatXml格式的数据集中取回预期数据
IDataSet expectedDataSet = new FlatXmlDataSet(new File("expectedDataSet.xml"));
ITable expectedTable = expectedDataSet.getTable("TABLE_NAME");
// 验证实际数据与预期数据是否一致
Assertion.assertEquals(expectedTable, actualTable);
}
注意也可以对数据集进行验证,也就是上面的第二个方法:assertEquals(IDataSet expected, IDataSet actual)
你也可以通过查询获得数据库实际数据,以下是例子:
ITable actualJoinData = getConnection().createQueryTable("RESULT_NAME",
"SELECT * FROM TABLE1, TABLE2 WHERE ...");
对于主键字段或日期时间字段,可能是由程序动态生成,无法预期它的值,所以在比较时可以告诉DBUnit不对这些字段验证,以下是例子:
ITable filteredTable = DefaultColumnFilter.includedColumnsTable(actual,
expected.getTableMetaData().getColumns());
Assertion.assertEquals(expected, filteredTable);
也就是在你的预期FlatXml文件中只需列出你关心的字段值即可
DBUnit与ANT能够集成,DBUnit提供有ANT自定义任务(TASK)扩展,这个DBUnit任务能够完成数据库初始化、数据库数据验证、数据库数据导出成XML文件等功能,这里我们以如何由当前数据库数据自动导成FlatXml文件的例子作说明,其它功能请参见javadoc。
手工编写FlatXml文件可能比较繁琐,DBUnit提供由当前数据库数据自动导出FlatXml文件的功能,之后我们可以基于这个导出的文件修改即可。导出的方式有两种,一种是通过编码实现;另一种是调用ANT任务。以下是调用ANT任务的使用说明:
u 定义dbunit任务
(1) 将DbUnit jar文件加到ant的lib目录下
(2) Build文件的开始处添加以下行
<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask"/>
u 调用dbunit任务
<dbunit driver="oracle.jdbc.driver.OracleDriver"
url="jdbc:oracle:thin:@10.164.22.163:1521:ise"
userid="tzs21911"
password="tzs21911"
schema="TZS21911"
classpath="C:/eclipse/lib/ojdbc14.jar">
<export dest="export.xml"/>
</dbunit>
上面的例子完成了这么几件事:通过dbunit元素属性告诉DBUnit要连接的数据库属性、通过export元素要求DBUnit导出数据库数据,属性dest是导出的文件名,属性format是导出的文件格式,默认格式是FlatXml。
在每个用例执行前,一般我们需要提供两个FlatXml文件,一个用于数据库初始化,一个用于用例执行后的数据库验证(预期结果)。我们在写自己的测试用例代码时注意以下几点:
u 继承自DBTestCase
u 设置数据库连接属性
u 实现getDataSet模板方法
u getSetUpOperation中指定初始化操作
u 调用类Assertion的方法进行验证
还要指出的是我们也可以不需要继承自DBTestCase也能完成数据库的测试,详细方法就不讲了,请大家参考我的例子代码(类dbunit.NoneDBTestCaseTest)。