DDTUnit 数据驱动框架介绍及其在单元测试中的应用

DDTUnit 是一个基于JUnit的以数据为驱动方法的测试框架。它通过提供一个简单的 XML 数据描述文件,把测试代码和测试数据剥离开来,进而实现了测试代码与数据无关。同时,该XML文件支持复杂自定义数据对象以及诸如collection,map等集合类。本文以实例探讨的方式深入挖掘DDTUnit的使用,并通过与传统的测试方法进行比较,让读者感受到使用DDTUnit所带来的好处。

刘 旭进, 软件工程师, IBM

刘旭进,IBM 中国开发中心,软件工程师 , 对开源软件,REST,Web Service,Open Search 有浓厚兴趣和深入研究。目前在 Lotus Connections Fils Team 从事 REST Service 开发相关工作。



2010 年 10 月 21 日

DDTUnit 概述

DDTUnit,是一个基于 JUnit 的以数据为驱动方法的测试框架。它通过提供一个简单的 XML 数据描述文件,把测试代码和测试数据剥离开来,进而实现了测试代码与数据无关。该 XML 文件支持复杂自定义数据对象以及诸如 Collection,Map 等集合类。

DDTUnit 数据驱动测试框架的基本思想

  1. 提供 DDTTestCase 测试基类,该类继承 JUnit TestCase 类,兼容传统测试方法。
  2. 提供支持自定义复杂数据对象的 XML Schema 定义。
  3. 通过 SAX parser 和 XML Schema 定义对用户自定义 XML 数据描述文件进行有效性验证。
  4. 通过解析 XML 数据描述文件获取用户自定义数据。
  5. 支持断言对象数据定义以及断言判定。

构建 DDTUnit 简单实例应用

  1. 下载 DDTUnit 二进制文件及其依赖库

    下载链接:http://ddtunit.sourceforge.net/,当前最新版本是 0.8.8。

    图 1. DDTUnit 下载
    图 1. DDTUnit 下载
  2. 在 RAD 中新建 Java Project 并把 ddtunit-0.8.8.jar 文件导入编译路径。同时 DDTUnit 本身依赖一些 Jar 包,同样需要下载解压并导入工程编译路径,下载地址同上,在此不再多说。
  3. 新建 XML 文件,命名为 DDT-TestResource.xml 将其存放在工程根目录,并修改其内容如下:
    清单 1. DDT-TestResource.xml
     <?xml version="1.0" encoding="UTF-8"?> 
     <ddtunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="../ddtunit.xsd"> 
      <cluster id="MyClassTest"> 
        <group id="testLocale"> 
          <test id="en"> 
            <objs> 
              <obj id="locale" type="String">en_US</obj> 
            </objs> 
            <asserts> 
              <assert id="result" action="isEqual" type="String">Hello, world!</assert> 
            </asserts> 
          </test> 
          <test id="cn"> 
            <objs> 
              <obj id="locale" type="String">zh_CN</obj> 
            </objs> 
            <asserts> 
              <assert id="result" action="isEqual" type="String"> 你好,世界! </assert> 
            </asserts> 
          </test> 
    
          <test id="jp"> 
            <objs> 
              <obj id="locale" type="String">ja_JP</obj> 
            </objs> 
            <asserts> 
              <assert id="result" action="isEqual" type="String"> こんにちは、世界! </assert> 
            </asserts> 
          </test> 
        </group> 
      </cluster> 
     </ddtunit>
  4. 新建类 DDTUnitTestBase 并继承 DDTTestCase 类,实现其 initContext() 方法如下:
    清单 2. 初始化
     public void initContext() { 
      initTestData("/DDT-TestResource.xml", "MyClassTest"); 
     }
  5. 新建被测试类 DemoTestCases,提供 sayHello() 方法,该方法可以根据输入 locale 不同输出不同的问候语。见清单 3:
    清单 3. SayHello 方法
     public String sayHello(String locale) 
     { 
      String resultStr = null; 
      if(locale==null||"".equals(locale)||"en_US".equals(locale)) 
      { 
        resultStr = "HelloWorld!"; 
      } 
      else if("zh_CN".equals(locale)) 
      { 
        resultStr = "你好世界!"; 
      } 
      else if("ja_JP".equals(locale)){ 
        resultStr = "こんにちは世界!"; 
      } 
      return resultStr; 
     }
  6. 新建类 DDTUnitTest 提供默认构造方法,并添加 testHelloWorld() 方法如下:
    清单 4. 第一个 TestCase
     public void testHelloWorld () 
     { 
     String locale = (String) getObject("locale"); 
        String result = sayHello(locale); 
        assertObject("result", result); 
     }
  7. 控制台输出
     INFO: [class com.ddtunit.test. DemoTestCases] (1/3) method "testHelloWorld", 
     test "en"
     Aug 26, 2010 6:31:32 PM junitx.ddtunit.DDTRunMonitor endMethodTest 
     INFO: [class com.ddtunit.test. DemoTestCases] (2/3) method "testHelloWorld", 
     test "zh"
     Aug 26, 2010 6:31:32 PM junitx.ddtunit.DDTRunMonitor endMethodTest 
     INFO: [class com.ddtunit.test. DemoTestCases] (3/3) method "testHelloWorld", 
     test "jp"
     Aug 26, 2010 6:31:32 PM junitx.ddtunit.DDTRunMonitor endTest 
     INFO: [class com.ddtunit.test.DemoTestCases] method "testHelloWorld": 
       3 of 3 test(s), 0 error(s), 0 failure(s)
  8. 我们的项目视图
    图 2. DDTUnitTest 工程
    图 2. DDTUnitTest 工程

DDTUnit 与传统测试方法比较

上述测试用例对 DemoTestCases 类的 sayHellWorld() 方法进行了测试。从这个例子我们可以体验到使用 DDTUnit 能给我们带来的好处。

对于 sayHelloWorld() 方法,试想如果简单的仅使用传统的 JUnit TestCase 进行测试,我们需要编写至少三个测试用例来测试:

  • public void testEnUSLocale();// 对应 locale = en _US 的情形
  • public void testZhCNLocale();// 对应 locale = zh_CN 的情形
  • public void testJaJPLocale();// 对应 locale = ja_JP 的情形

另外,在例子中只是测试英文、中文、日文的情况,在复杂的情况,如果我们要测试的 locale 更多的情况呢?比如德文、法文、阿拉伯文等。那么我们要相应的编写新的测试用例、调试、运行等,这无疑是一个巨大的工作量。

而在使用 DDTUnit 后,无论我们需要测试多少种可能的 locale, 我们只需要上述一个测试用例,即使在需要新增测试用例时,我们所需要做的只是维护 DDT-TestResource.xml 文件,而不用重新编写新的测试用例。更为方便的是,DDTUnit 使我们在修改 XML 数据文件后,不需要重新编译 Java 文件,而可以直接运行新增测试用例。


构建 DDT 模式下复杂数据类型对象

DDTUnit 不仅支持简单数据类型的作为测试数据,而且支持自定义复杂自定义数据对象以及对诸如 Collection,Map 等集合类的判定。

在下面的例子中我们自定义 Money 类,该类有 yuan、jiao、fen 三个属性,我们将测试该类实例相加的情况。请参见下面的代码:

清单 5. Money 类定义
 package com.ddtunit.test.bean; 

 public class Money { 

  public int yuan; 
  public int jiao; 
  public int fen; 
  
  ... ... 
  
  public Money add(Money mon) 
  { 
    int yu; 
    int ji; 
    int fe; 
    
    yu = this.yuan + mon.yuan; 
    ji = this.jiao + mon.jiao; 
    fe = this.fen + mon.fen; 
  
    return new Money(yu,ji,fe); 
  } 
  
  public boolean equals(Object obj) 
  { 
    if((obj==null)||!(obj instanceof Money)) 
    { 
      return false; 
    } 
    Money mon = (Money)obj; 
    if((this.yuan == mon.yuan)&&(this.jiao == mon.jiao)&&(this.fen == mon.fen)) 
    { 
      return true; 
    } 
    else{ 
      return false; 
    } 
  } 
  
  public int hashcode() 
  { 
    return this.yuan*100+this.jiao*10+this.fen; 
  } 
 }
清单 6. 数据定义文件
 <group id="testMoney"> 
  <test id="positiveAdd"> 
    <objs> 
      <obj id="number1" type="com.ddtunit.test.bean.Money"> 
        <yuan>4</yuan> 
        <jiao>5</jiao> 
        <fen>6</fen> 
      </obj> 
      <obj id="number2" type="com.ddtunit.test.bean.Money"> 
        <yuan>1</yuan> 
        <jiao>2</jiao> 
        <fen>3</fen> 
      </obj>          
    </objs> 
    <asserts> 
      <assert id="result" action="isEqual" type="com.ddtunit.test.bean.Money"> 
        <yuan>5</yuan> 
        <jiao>7</jiao> 
        <fen>9</fen>          
      </assert> 
    </asserts> 
  </test>    
  <test id="negativeAdd"> 
    <objs> 
      <obj id="number1" type="com.ddtunit.test.bean.Money"> 
        <yuan>4</yuan> 
        <jiao>5</jiao> 
        <fen>6</fen> 
      </obj> 
      <obj id="number2" type="com.ddtunit.test.bean.Money"> 
        <yuan>-1</yuan> 
        <jiao>-2</jiao> 
        <fen>-3</fen> 
      </obj>          
    </objs> 
    <asserts> 
      <assert id="result" action="isEqual" type="com.ddtunit.test.bean.Money"> 
        <yuan>3</yuan> 
        <jiao>3</jiao> 
        <fen>3</fen>          
      </assert> 
    </asserts> 
  </test>              
 </group>
清单 7. 测试用例
 public void testMoney() 
 { 
  Money mon1 = (Money)getObject("number1"); 
  Money mon2 = (Money)getObject("number2");     
  Money result = mon1.add(mon2); 
  addObjectToAssert("result",result); 
 }

上例采用用户自定义 Money 类作为测试数据类型,详细说明了 DDTUnit 对复杂对象作为数据类型情况下的支持与使用方法。通过支持用户自定义数据类型,在很大程度上扩展了 DDTUnit 的应用广度与深度。


用户自定义数据类型方式

DDTUnit 框架功能的强大很大程度在于它提供了对用户自定义数据类型的支持。用户通过自定义的数据类型,可以进行更广泛意义上的数据内容的获取并对其进行断言。

DDTUnit 支持下列自定义数据类型方式:

  1. 通过默认构造函数和属性
  2. 通过默认构造函数和 Bean
  3. 通过默认构造函数与方法调用
  4. 定义常量对象
  5. 定义数组类型
  6. 定义容器类型,包括 Collection 和 Map
  7. 定义断言对象

下面我们分别介绍这几种自定义数据类型的方式。

通过默认构造函数和类属性

清单 8. XML 定义 Schema
 <objs> 
 <obj id="myId" type="my.class.Type" int="fields"> 
 ... 
 </obj> 
 <objs>

通过类构造函数和属性声明对象的前提是要使用确切的对象的属性名,DDTUnit 以此通过 Java 的反射机制来调用类构造函数并对其属性赋的相应的值。

清单 9. XML 示例
 <test id="myFirstTestCase"> 
   <objs> 
      <obj id="myObj" type="junitx.ddtunit.resources.SimpleVO"> 
         <doubleValue>12.4</doubleValue> 
         <integerValue>4711</integerValue> 
         <stringValue>My Text</stringValue> 
      </obj> 
    </objs> 
 </test>
清单 10. 自定义类实现
 public class SimpleVO { 
    private Integer integerValue; 
    private String stringValue; 
    private Double doubleValue; 

    /** 
     * Default constructor. 
     */ 
    public SimpleVO() { 
        // no special initialization neccessary 
 } 
 }

通过默认构造函数和 Bean

清单 11. XML 定义 Schema
 <objs> 
 <obj id="myId" type="my.class.Type" hint="bean"> 
 ... 
 </obj> 
 <objs>

通过类构造函数和 Bean 声明对象的前提是要使用确切的对象的 setter 方法名以及输入参数类型,DDTUnit 以此通过 Java 的反射机制来调用类构造函数并对其属性赋的相应的值。

清单 12. XML 示例
 <test id="myFirstTestCase"> 
   <objs> 
       <obj id="myObj" type="junitx.ddtunit.resources.SimpleVO"> 
         <doubleValue>12.4</doubleValue> 
         <integerValue>4711</integerValue> 
       </obj> 
   </objs> 
 </test>
清单 13. 自定义类实现
 public class SimpleVO { 
    private Integer integerValue; 
    private String stringValue; 
    private Double doubleValue; 

    /** 
     * Default constructor. 
     */ 
    public SimpleVO() { 
        // no special initialization neccessary 
 } 

  public void setDoubleValue(Double arg){ 
      // whatever check or calculation you like to process ... 
      this.doubleValue = arg; 
    } 

    public void setIntegerValue(Integer arg){ 
      // whatever check or calculation you like to process ... 
      this.integerValue = arg; 
    } 
 }

通过构造函数与方法调用

清单 14. XML 定义 Schema
 <objs> 
 <obj id="myId" type="my.class.Type" hint="call" calltype="my.call.Type" method="" > 
 ... 
 </obj> 
 <objs>

通过类构造函数和方法调用声明对象 , 用户可以指定构造函数甚至是某一静态工厂类的方法。 DDTUnit 通过用户指定方法利用 Java 的反射机制来调用类构造函数并对其属性赋的相应的值。

清单 15. XML 示例
 <obj id="myObj" type="junitx.ddtunit.resources.SimpleVO" 
  calltype="junitx.ddtunit.resources.SimpleVO" hint="call" method="constructor"> 
     <item type="string">My Text</item> 
     <item type="int">4711</item> 
     <item type="double">12.4</item> 
 </obj>

其中:

hint=call: 表示使用构造函数或者指定方法创建对象。

method=methodName: 指定调用方法名,调用方法为构造函数时可以设定 method=constructor。如果调用方法没有输入参数时,可以指定 method=toString。

Calltype=my.call.Type:指定调用方法所属类。

清单 16. 自定义类实现
 public class SimpleVO { 
 ... 
    public SimpleVO(String text, Integer intValue, Double doubValue) { 
        // whatever your class should do on instanciation 
    } 
 ... 
 }

定义常量对象

清单 17. XML 定义 Schema
 <objs> 
 <obj id="myId" type="my.class.Type" hint="constant"> 
 ... 
 </obj> 
 <objs>

需要说明的一点是,type 指定的常量所属类型,这个常量必须是类静态常量。

清单 18. XML 示例:
 <test id="myFirstTestCase"> 
   <objs> 
      <obj id="myObj" type="junitx.ddtunit.resources.SimpleConstants"
        hint="constant"> 
 MY_STRING_CONSTANT 
 </obj> 
   </objs> 
 </test>
清单 19. 自定义类实现
 public class SimpleConstants { 
  public final static String MY_STRING_CONSTANT = "Hallo World"; 
  /** 
   * Default constructor. 
   */ 
  private SimpleConstants() { 
    // no special initialization neccessary 
  }

使用数组类型

清单 20. XML 定义 Schema
 <objs> 
 <obj id="myId" type="my.class.Type" hint="array" > 
 ... 
 </obj> 
 <objs>
清单 21. XML 示例
 <test id="myFirstTestCase"> 
   <objs> 
     <obj id="myObj" type="int" hint="array"> 
       <item>4711</item> 
       <item>4712</item> 
     </obj> 
   </objs> 
 </test>`
清单 22. 自定义类实现
 Integer [] myObj = { new Integer(4711), new Integer(4712)};

使用 Collection 和 Map 容器类型

清单 23. XML 定义 Schema
 Collection: <objs><obj id="myId" type="my.class.Type" hint="collection">...</obj><objs> 

 Map: <objs><obj id="myId" type="my.class.Type" hint="map">...</obj><objs>
清单 24. XML 示例:
Collection: 
 <obj id="myVector" type="java.util.Vector" hint="collection"> 
    <item type="string">firstEntry</item> 
    <item type="string">secondEntry</item> 
    <item type="string">thirdEntry</item> 
 </obj> 

 Map: 
 <obj id="myMap" type="java.util.HashMap" hint="map"> 
    <item> 
      <key type="java.lang.String">firstkey</key> 
      <value type="java.lang.String">firstValue</value> 
    </item> 
    <item> 
      <key type="java.lang.String">secondkey</key> 
      <value type="java.lang.String">secondValue</value> 
    </item> 
 </obj>
清单 25. 自定义类实现
Collection:
 Vector vec = new Vector(); 
 vec.put("firstEntry"); 
 vec.put("secondEntry"); 
 vec.put("secondEntry"); 

 Map:
 Map map = new Hashmap(); 
 map.put(firstkey, firstValue); 
 map.put(secondkey, secondValue);

定义断言对象

清单 26. XML 定义 Schema
 <asserts> 
 <assert id="myId" type="my.class.Type" hint="myParseHint" action="MYACTION"> 
 ... 
 </assert> 
 <asserts>

DDTUnit 支持非常灵活的断言方法,其中 action 可选的值包括下面的字符串:

表 1. action 属性可选值
名称描述
isEqual 对应 JUnit Assert.assertEquals().
isNotEqual 对应 JUnit-Addons Assert.assertNotEquals().
isSame 对应 JUnit Assert.assertSame().
isNotSame 对应 JUnit-Addons Assert.assertNotSame().
isNull 对应 JUnit Assert.assertNull().
isNotNull 对应 JUnit-Addons Assert.assertNotNull().
isTrue 对应 JUnit Assert.assertTrue().
isFalse 对应 JUnit-Addons Assert.assertFalse().
isContainedIn 对应 JUnit-Addons Assert.assertTrue(((Collection)obj).contains(actual)).
isNotContainedIn 对应 JUnit-Addons Assert.assertFalse(((Collection)obj).contains(actual)).
isLT 实际小于预期
isNotLT 实际不小于预期
isGT 实际大于预期
isNotGT 实际不大于预期
isInRange 实际在预期范围内
isNotInRange 实际不在预期范围内
清单 27. XML 示例
 <assert id="complete" action="isEqual" type="junitx.ddtunit.resources.SimpleVO"> 
  <doubleValue>12.4</doubleValue> 
  <integerValue>4711</integerValue> 
  <stringValue>My Text</stringValue> 
 </assert>

所有以前面方式声明的类型都可以在 Assert 断言中定义。有一点需要注意的是必须覆盖类的 equals() 和 hashCode() 方法,从而 DDTUnit 可以通过这两个方法进行对象的比较。

清单 28. Java 实现
 public void testMyService(){ 
  SimpleVO simpleVO = MyServiceUnderTest.getSimpleVO(); 
  addObjectToAssert("complete", simpleVO); 
 }

其中方法 public addObjectToAssert(String id, Object toAssertAgainst),提供了断言的方式。


总结

本文对 DDTUnit 做了一个系统的介绍,并通过实例详细介绍了 DDTUnit 这种新的测试框架的使用。


下载

描述名字大小
样例代码DDTUnitTest.zip2MB

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=555463
ArticleTitle=DDTUnit 数据驱动框架介绍及其在单元测试中的应用
publish-date=10212010