Java单元测试技术2

1      测试桩构建(EasyMock

构造测试桩太麻烦是项目组抱怨单元测试难做的主要原因之一,尤其是WEB应用程序开发,大量对象是由WEB容器生成,如HttpServletRequestHttpServletResponseServletContext等,只有将程序布署到服务器上才能获得这些对象,这样带来的麻烦是:一方面被测对象难于孤立,输入输出难以自由控制;另一方面每次运行都要将代码布署到服务器上很浪费时间,无法脱离服务器独立运行。目前构建测试桩的首选工具是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类是EasyMockFaç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.0Generic 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  方法的参数比较规则是以方法调用形式实现的,另外还可以自定义比较器

2      数据库数据初始化与测试验证(DBUnit

前面已经提到,业软的很多产品访问数据库的代码量占很大比例,目前各版本开发中访问数据库的技术有很多,如JDBCEntityBeanHibernateSpringiBATIS等,不管你使用哪种技术,实际上归根结蒂都是通过Java代码访问数据库,对于这些访问数据库的代码的单元测试长期以来存在以下问题:首先,如果将数据库层用取代,一方面构建的工作量巨大,另一方面即使能够构建完成,其实也不太容易发现代码中的BUG,因为隔离了数据库后其实代码逻辑就比较简单了,综合考虑,我们的建议是只有真实地连数据库才能真正有效地对单元做测试;但真实地连数据库又会带来另外两个问题:

u  如何确保每个用例执行前的数据库环境是可预期的?也就是数据库的初始化问题。

u  如何确保用例执行过程中正确地操作了数据库?也就是用例执行后的数据库验证。

通过在项目组的推广试用,我们发现DBUnit很好地解决了这两个问题,它的主要功能包括测试前初始化数据库数据,测试结束后验证数据库数据,另外,DBUnit提供有自定义ANT任务,结合ANT实现前述功能。要使用DBUnit也是只需要在你的CLASSPATH路径中加上它的JAR包。

1)        测试用例框架

DBUnit提供有基础类DBTestCase,该类继承自JUnitTestCase,写测试用例的方法同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是表名,IDNAMEAGE是表字段,=后面表示字段值。可以在一个文件中初始化多个表,但如果表之间有约束关系(如外键),要注意表的先后顺序。

该方法返回数据集(DataSet),注意数据集可以是多个表的集合

u  说明三

这是告诉DBUnit在执行用例前(测试函数运行前,在setUp方法里被调用到)要执行的数据库初始化操作(Operation),数据的来源就是前面提到的getDataSet方法返回的数据集。有多个操作类型,常用的有:

DatabaseOperation.INSERT

往数据库表里插入数据集中的数据,前提条件是数据库表里没有这些数据。

DatabaseOperation.DELETE

删除数据库里数据集包含的记录,注意数据集里不含有的记录不删除。

DatabaseOperation.DELETE_ALL

删除数据库里数据集指定的表的所有记录。

DatabaseOperation.CLEAN_INSERT

先执行DELETE_ALL操作,再执行INSERT操作,这是最常用的操作。

2)        数据库验证

在运行了被测方法后,我们需要验证该方法对数据库的操作是否正确,以下是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)

3)        使用查询获得数据库快照

你也可以通过查询获得数据库实际数据,以下是例子:

ITable actualJoinData = getConnection().createQueryTable("RESULT_NAME",

                "SELECT * FROM TABLE1, TABLE2 WHERE ...");

4)        比较时忽略某些列(字段)

对于主键字段或日期时间字段,可能是由程序动态生成,无法预期它的值,所以在比较时可以告诉DBUnit不对这些字段验证,以下是例子:

    ITable filteredTable = DefaultColumnFilter.includedColumnsTable(actual,

            expected.getTableMetaData().getColumns());

    Assertion.assertEquals(expected, filteredTable);

也就是在你的预期FlatXml文件中只需列出你关心的字段值即可

5)        DBUnitANT的集成

DBUnitANT能够集成,DBUnit提供有ANT自定义任务(TASK)扩展,这个DBUnit任务能够完成数据库初始化、数据库数据验证、数据库数据导出成XML文件等功能,这里我们以如何由当前数据库数据自动导成FlatXml文件的例子作说明,其它功能请参见javadoc

手工编写FlatXml文件可能比较繁琐,DBUnit提供由当前数据库数据自动导出FlatXml文件的功能,之后我们可以基于这个导出的文件修改即可。导出的方式有两种,一种是通过编码实现;另一种是调用ANT任务。以下是调用ANT任务的使用说明:

u  定义dbunit任务

(1)       DbUnit jar文件加到antlib目录下

(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

6)        小结

在每个用例执行前,一般我们需要提供两个FlatXml文件,一个用于数据库初始化,一个用于用例执行后的数据库验证(预期结果)。我们在写自己的测试用例代码时注意以下几点:

u  继承自DBTestCase

u  设置数据库连接属性

u  实现getDataSet模板方法

u  getSetUpOperation中指定初始化操作

u  调用类Assertion的方法进行验证

还要指出的是我们也可以不需要继承自DBTestCase也能完成数据库的测试,详细方法就不讲了,请大家参考我的例子代码(类dbunit.NoneDBTestCaseTest)。

 

原文地址:https://www.cnblogs.com/javawebsoa/p/3089231.html