级别: 中级
贾 少亮 (jiasl@cn.ibm.com), 软件工程师, IBM
2008 年 10 月 21 日
“规则”(Rule)在程序中有广泛的应用,本文将介绍在对“规则”进行单元测试(Unit Testing)时的一些独特之处。希望能与读者探讨。
我们先看一个例子,以了解对”规则”做单元测试的特点。我们有一个性能调优工具 WPA, 它能够将与性能相关的参数的值进行评估并推荐最优值。它的评估和推荐最优值算法都是基于”规则”的。
Java 虚拟机的初始堆大小(JVM initial heap size)是一个影响 JVM 的性能的关键参数。性能调优工具 WPA 有一套规则对“ JVM initial heap size ”的值进行评估(参见清单 1)。评估的结果有 5 个级别。级别“ 1 ”表示设置良好,可提高性能;级别“ 5 ”表示设置很差,会降低性能。
清单 1. JVM initial heap size rating algorithm
|
在 这一套规则中,有三个输入参数:“initialHeapSize”(“JVM initial heap size”的值),“currentMemoryPoolSize” ( 内存池的值 ) 和“overallMemoryOnPartition”(物理内存的值)。为了得到这些值,我们需要使用 Application Server 和 OS 提供的 API 。在使用这些 API 的时候,我们必须构造出 API 所需的运行环境。
在这一套规则中,包含很多不同的条件(见“ IF-ELSE ”语句)。在测试时(单元测试和功能测试),我们需要至少 24 组测试数据以覆盖所有的阀值(threshold value)和等价类(equivalent class)。参见表 1。
从“JVM initial heap size rating algorithm”以及 WPA 中其他基于“规则”的性能调优算法,我们总结出对“规则”做单元测试的特点有:
一、 为了覆盖所有的阀值 (threshold value )和等价类 (equivalent class ),我们需要大量测试数据。单元测试的通常做法是,把所有的测试数据写入测试代码中。对比以格式化的形式(XML,Excel 等)来保存测试数据,这样做使得这些数据不容易维护和复用。
二、由于对”规则”的测试涉及到变量,这些变量来自运行时的输入,我们在单元测试之前就需要构建运行时环境,这种工作可能非常复杂。如果一套”规则”中包含更多的条件和输入参数,以上两个问题会更加严重
三、在一个基于”规则”的系统里,”规则”之间有很多共性,我们没有必要对每一个”规则”都写一个测试类。
本文将给出解决以上问题的一种做法。本文的组织结构如下:
- 编写 Mock 类:利用 Mock 对象来代替实时运行环境;
- 将测试数据保存到配置文件中:利用格式化文档实现测试数据的复用性和可维护性;
- 编写 SettersMap 类:这个类保存了配置文件中的数据并提供了获取这些数据的接口;
- 编写可复用的 TestCase 类:创建 JUnit 的扩展类以适应对“规则”做单元测试的需求;
- 用 TestSuite 组织测试用例:用 TestSuite 把测试用例组织起来;
- 在以下内容中,我们将拿“ JVM initial heap size rating algorithm ”做例子。
为了测试” JVM initial heap size rating algorithm ”,我们需要获得三个输入参数。然而,获取这三个参数并不是那么容易。
为了简化测试环境,我们利用 Mock 对象来设置这些参数。
Mock 对象是单元测试经常用到的一种技术,Mock 对象能模拟实际对象的行为,并且提供了额外的行为控制接口。还有一个常用到的词是 Dummy 对象。 Mock 和 Dummy 的含义经常被混淆。在这里,我们认为 Dummy 对象没有提供额外的行为控制接口。
对于” JVM initial heap size rating algorithm ”,我们需要一个 Mock 类,它的行为与“ InitialHeapSize.java ”相同(“ InitialHeapSize.java ”是 “ JVM initial heap size rating algorithm ”的 Java 代码)。我们把这个 Mock 类命名为“ MockInitialHeapSize.java ”。一个 Client 类可以把“ initialHeapSize ” , “ currentMemoryPoolSize ” , 和“ overallMemoryOnPartition ” 直接设置到“ MockInitialHeapSize ”对象中。参见清单 2
清单 2. MockInitialHeapSize.java
|
正如我们在文章开头提到的,我们希望把测试数据保存成格式化的形式,以便对这些数据进行维护和复用。表 1展示了用一个 Excel 文件 “ MockInitialHeapSize_rating.xls ” 保存所有的测试数据的例子。 这个文件完全可以用于功能测试的文档编写。
表 1. JVM initial heap size 测试数据
setInitialValue | mockSetOverallMemory | mockSetMemoryPoolSize | result |
---|---|---|---|
31 | 92 | 92 | 5 |
31 | 123 | 123 | 5 |
31 | 124 | 124 | 5 |
32 | 95 | 95 | 5 |
32 | 127 | 127 | 3 |
32 | 128 | 128 | 3 |
47 | 140 | 140 | 5 |
47 | 187 | 187 | 3 |
47 | 188 | 188 | 3 |
48 | 143 | 143 | 5 |
48 | 191 | 191 | 3 |
48 | 192 | 192 | 1 |
49 | 146 | 146 | 5 |
49 | 195 | 195 | 3 |
49 | 196 | 196 | 1 |
1024 | 3071 | 3071 | 5 |
1024 | 4095 | 4095 | 3 |
1024 | 4096 | 4096 | 1 |
1025 | 3074 | 3074 | 5 |
1025 | 4009 | 4009 | 3 |
1025 | 4100 | 4100 | 3 |
1537 | 4610 | 4610 | 5 |
1537 | 6147 | 6147 | 5 |
1537 | 6148 | 6148 | 5 |
表 1中, 每一行都代表了一组测试数据,包括输入参数和期望结果。三个输入参数 “initialHeapSize”,“currentMemoryPoolSize”,“overallMemoryOnPartition”分别保存 到了三列中:“setInitialValue”,“mockSetOverallMemory ”和“mockSetMemoryPoolSize”。期望结果保存到了“result”列 ,测试代码将从这个文件中获取测试数据。
配置文件的格式是可以变化的,只需要提供相应的 SettersMap 和 SettersMapFactory 类就可以了。参看下文。
有了配置文件,我们需要编写代码从配置文件中读取测试数据。我们用一个接口类“SettersMap”来代表一个配置文件。参见图 1。附件“rule_test.zip”中的 BaseSettersMap.java 是 SettersMap 接口的一个实现。
图 1. SettersMap.java
我们提供了一个工厂接口 SettersMapFactory 来构造 SettersMap 。这里采用了抽象工厂(Abstract Factory)的设计模式。
清单 3. SettersMapFactory.java
|
对于不同的文件格式,需要提供不同的“ SettersMapFactory ”。附件“ rule_test.zip “中的“ ExcelSettersMapFactory.java ”是一个 Excel 格式的实现。
在一个基于”规则”的系统里,”规则”之间有很多共性,我们没有必要对每一个”规则”都写一个测试类,而是希望能有一个通用的类,通过改变参数来测试不同的规则。
标 准的 JUnit 版本 (www.junit.org) 提供了 junit.framework.TestCase 类作为单元测试的一个最常用的入口。通常,我们有两种方式来运行 TestCase:对象方式和类方式。在对象方式运行时,你需要 new 一个 TestCase 对象,并且在构造函数中指定 Test Method 的名字。运行时,只有这个 Test Method 会被调用。在类方式下,所有的以”test”开头的方法都会被调用,但是我们无法复用这个类。 这两种方式都不能满足我们的需求。幸运的是,我们可以通过 扩展“junit.framework.TestCase”来做到这一点。
清单 4. ObjectTestCase.java
|
我 们将构造一个“ObjectTestCase”类,这个类继承了“TestCase”类。“ObjectTestCase”使用一个 ArrayList “testMethods” 来保存所有的 Test Method 。在实例化“ObjectTestCase”时,所有以“test”开头的方法都会被注册到“testMethods”中。在“runTest”时,所有 的保存在 “testMethods”中的方法都会被调用 . 最后,别忘了复写“countTestCases”以保证我们获得正确的测试结果。
编写专用于“规则”的 AttirbuteTestCase 类
有了“ObjectTestCase”类,我们就可以扩展它以获得针对“规则”的“TestCase”类。图 2 展示了这些类之间的关系。“AttributeTestCase”是一个抽象类,它继承于“ObjectTestCase”。“testAttribute”是它的一个抽象方法,需要它的子类提供具体实现。这个方法会测试所有的数据。
图 2. TestCase Class Diagram
“AttributeRatingTestCase” 和“AttributeRecommendationTestCase”继承了“AttributeTestCase”。以 “AttributeRatingTestCase”为例,它的“testAttribute”方法首先获得“SettersMap”,然后调用 “setInput”把 SettersMap 中的数据设置到 Mock 对象中;最后,它调用 Mock 对象的“getRating”方法获取结果。参见清单 5。我们在配置文件中,把每一列的列名设置为 Mock 对象的 Mock 方法名,这样,测试框架就明确的知道应该调用 Mock 对象的什么方法来设置数据。为了做到这一点,撰写配置文件时,必须知道相应的 Mock 方法名 ( 如 MockInitialHeapSize.mockSetMemoryPoolSize) 。由于我们在讨论单元测试,我们认为测试人员拥有这些测试代码,也就是知道 Mock 方法名。
清单 5. AttributeRatingTestCase.java
|
由 于我们构造了自己的 TestCase, TestSuite 常用的组织 TestCase 的方法需要做一点小小的改动。在我们的 TestSuite 中,提供了一个方法“ addTestCase ”。这个方法可以将 TestCase 添加到 TestSuite 中。参见清单 6。
清单 6. addTestCase method
|
有了 addTestCase 方法 , 我们就可以轻易的把 TestCase 添加到 TestSuite 中了。参见清单 7。
清单 7. LWIAttributesRatingTestSuite.java
|
如 果你有很多的 TestSuite, 你应该把他们很好的组织起来。在我们的测试框架中, 一个 TestSuite 在其实例化阶段添加所有的 TestCase 。这就意味着我们只要拥有一个 TestSuite 的实例,我们就拥有了它所包含的 TestCase 。这样 , 一个 AllTest 类可以以如下方式来编写 :
清单 8. AllTest.java
|
测 试用例的组织可以用下图来说明。图中,每一个矩形都代表了一个“TestSuite”类。“TestSuite ”类以树形结构组织起来。你可以调用任何一个类的“main”方法来执行以这个类为树根的子树下的所有测试用例。以“WASAllTest”类为例,执行 它的“main”方法将测试 “WASRecTestSuite”和 “WASRatingTestSuite”中的所有测试用例。
本文介绍了在对规则进行单元测试时实现可配置性和复用性。我们也介绍了一些常用的单元测试技术,比如使用 Mock 对象和扩展 JUnit 。这些技术可以使用到任何其他的单元测试中。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
本文用到的部分 Java 代码示例 | rule_test.zip | 813KB | HTTP |
学习
- 了解:“阀值的介绍”。
- 了解:“等价类的介绍”。
- 教程 如何编写自定义的匹配符。
- “JUnit 4 抢先看”(developerWorks,2005 年 10 月):本文以 JUnit 4 为例,详细介绍了如何在自己的工作中使用这个新框架。
- “追求代码质量: JUnit 4 与 TestNG 的对比”(developerWorks,2006 年 9 月):JUnit 4 具有基于注释的新框架,它包含了 TestNG 一些最优异的特性。但这是否意味着 JUnit 4 已经淘汰了 TestNG?Andrew Glover 探讨了这两种框架各自的独特之处,并阐述了 TestNG 独有的三种高级测试特性。
- “深入探索 JUnit 4”(developerWorks,2007 年 3 月):本教程介绍了如何充分利用由注释实现的 JUnit 4 新功能,包括参数测试、异常测试及计时测试。
- “单元测试利器 JUnit 4”(developerWorks,2007 年 2 月):本文主要介绍了如何使用 JUnit 4 提供的各种功能开展有效的单元测试,并通过一个实例演示了如何使用 Ant 执行自动化的单元测试。
- 通过 popper 工程 了解更多的理论(Theory)知识。
获得产品和技术
- 从 JUnit 官方网站 下载 JUnit 4.4。
贾少亮是一名 IBM CSTL 的开发人员。他拥有5年的 Java 开发经验。他目前负责的项目是一个 Web 性能监控及评价系统。他对于单元测试的自动化有浓厚的兴趣,有过很多实践,以及自己独到的心得。 |