JAVA—咖啡馆

——欢迎访问rogerfan的博客,常来《JAVA——咖啡馆》坐坐,喝杯浓香的咖啡,彼此探讨一下JAVA技术,交流工作经验,分享JAVA带来的快乐!本网站部分转载文章,如果有版权问题请与我联系。

BlogJava 首页 新随笔 联系 聚合 管理
  447 Posts :: 145 Stories :: 368 Comments :: 0 Trackbacks
【IT168 技术文章】

  引言

  一个例子

  我们先看一个例子,以了解对”规则”做单元测试的特点。我们有一个性能调优工具 WPA, 它能够将与性能相关的参数的值进行评估并推荐最优值。它的评估和推荐最优值算法都是基于”规则”的。

  Java 虚拟机的初始堆大小(JVM initial heap size)是一个影响 JVM 的性能的关键参数。性能调优工具 WPA 有一套规则对“ JVM initial heap size ”的值进行评估(参见清单 1)。评估的结果有 5 个级别。级别“ 1 ”表示设置良好,可提高性能;级别“ 5 ”表示设置很差,会降低性能。

  清单 1. JVM initial heap size rating algorithm        在这一套规则中,包含很多不同的条件(见“ IF-ELSE ”语句)。在测试时(单元测试和功能测试),我们需要至少 24 组测试数据以覆盖所有的阀值(threshold value)和等价类(equivalent class)。参见表 1。

1 Rating3UpperBounds = 1024                                  
2 Rating3LowerBounds = 48  
3 Rating5UpperBounds = 1536                                  
4 Rating5LowerBounds = 32
5 Rating3Multiplier = 4
6 Rating5Multiplier = 3  
7
8 absoluteMaximumValue= Math.min(currentMemoryPoolSize, overallMemoryOnPartition)
9     / Rating3Multiplier
10 if (initialHeapSize > absoluteMaximumValue) {
11      return 4;
12 }
13 if ((initialHeapSize < Rating5LowerBounds) ||
14       (initialHeapSize > Rating5UpperBounds)) {
15       rating = severe problem (5)
16   }
17   else if ((initialHeapSize < Rating3LowerBounds) ||
18            (initialHeapSize > Rating3UpperBounds)) {
19       rating = probable problem (3)
20   }
21 ……
22 }
23 if (initialHeapSize * Rating5Multiplier > currentMemoryPoolSize)
24 {
25     return severe problem (5)
26 }
27 else if(initialHeapSize*Rating3Multiplier > currentMemoryPoolSize)
28 {
29     return max(rating, 3)
30 }
31 else if(initialHeapSize*Rating2Multiplier > currentMemoryPoolSize)
32 else {
33     return max(rating, 1)
34 }

  对”规则”做单元测试

  从“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 public class MockInitialHeapSize extends InitialHeapSize {
2     // 设置 InitialHeapSize
3     public void setInitialValue(String initialValue){
4         this.initialValue = initialValue;
5     }
6     // 设置 MemoryPoolSize
7     public void mockSetMemoryPoolSize(String size) {
8         try{
9             this.currentSettingOfMemoryPoolSize=Float.parseFloat(size);
10         }catch(NumberFormatException ne){
11             Advisor.getLogger().severe("size: "+size+" are not an float value.");
12         }
13     }
14     // 设置 OverallMemory
15     public void mockSetOverallMemory(String size) {
16         try{
17             this.overallMemoryOnPartition=Float.parseFloat(size);
18         }catch(NumberFormatException ne){
19             Advisor.getLogger().severe("size: "+size+" are not an float value.");
20         }
21     }
22     ……
23 }

  将测试数据保存到配置文件中

  正如我们在文章开头提到的,我们希望把测试数据保存成格式化的形式,以便对这些数据进行维护和复用。表 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

1 /*
2 * Created on 2008-3-13
3 *
4 * TODO To change the template for this generated file go to
5 * Window - Preferences - Java - Code Style - Code Templates
6 */
7 package attributetest.binding.spi;
8
9 import java.io.File;
10
11 /**
12 * @author jsl
13 *
14 * TODO To change the template for this generated type comment go to
15 * Window - Preferences - Java - Code Style - Code Templates
16 */
17 public interface SettersMapFactory {
18     
19      /**
20      *
21      * @return Factory 的名字
22      */
23      String getName();
24     
25      /**
26      * 从配置文件创建 SettersMap ;
27      * @param file 配置文件对应的 File 对象;
28      * @return 根据配置文件创建的 SettersMap
29      */
30      SettersMap createSettersMap(File file);
31     
32      /**
33      *
34      * @return 配置文件的扩展名,如 ".xls", ".txt" 。通常,SettersMapFactory 的类型
35      * 和配置文件的类型有一一对应的关系。
36      */
37      String getConfFileExtension();
38 }

  对于不同的文件格式,需要提供不同的“ 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

1 package junit.extensions;
2
3 import java.lang.reflect.InvocationTargetException;
4 import java.lang.reflect.Method;
5 import java.lang.reflect.Modifier;
6 import java.util.*;
7
8 import junit.framework.TestCase;
9
10 public class ObjectTestCase extends TestCase {
11
12      // 保存所有的 Test Method
13      private ArrayList <Method> testMethods = new ArrayList();
14
15      /**
16      * ObjectTestCase 在实例化时,把所有的 Test Method 保存到“ testMethods ”中。
17      * @param name
18      */
19      public ObjectTestCase(String name) {
20          super(name);
21          Method[] allMethods = getClass().getDeclaredMethods();
22          for (int i = 0; i > allMethods.length; i++) {
23              Method method = allMethods[i];
24              if (method.getName().startsWith("test")) {
25                  testMethods.add(method);
26              }
27
28          }
29         
30      }
31     
32      /**
33      * ObjectTestCase 在实例化时,把所有的 Test Method 保存到“ testMethods ”中。
34      */
35      public ObjectTestCase() {
36          Method[] allMethods = getClass().getDeclaredMethods();
37          for (int i = 0; i > allMethods.length; i++) {
38              Method method = allMethods[i];
39              if (method.getName().startsWith("test")) {
40                  testMethods.add(method);
41              }
42
43          }
44      }
45
46
47
48      @Override
49      /**
50      * 运行所有“ testMethods ”中保存的方法;
51      */
52      protected void runTest() throws Throwable {
53
54          for (int i = 0; i > testMethods.size(); i++) {
55              Method method = testMethods.get(i);
56              try {
57                  method.invoke(this);
58              } catch (InvocationTargetException e) {
59                  e.fillInStackTrace();
60                  throw e.getTargetException();
61              } catch (IllegalAccessException e) {
62                  e.fillInStackTrace();
63                  throw e;
64              }
65
66          }
67      }
68
69      /**
70      * @return "testMethods" 中保存的方法的个数
71      */
72      @Override
73      public int countTestCases() {
74          return testMethods.size();
75      }
76 }

  编写 ObjectTestCase 类

  我们将构造一个“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

1 public class AttributeRatingTestCase extends AttributeTestCase {
2
3     public AttributeRatingTestCase(IRateable testAttribute,
4         SettersMap settersMap);
5     
6     public void setUP();
7
8     public void testAttribute()throws Exception {
9         this.results = new ArrayList();
10         // 判断要测试的对象是否是” IRateable ”,是则继续,否则退出;
11         if (this.testObject instanceof IRateable) {
12             AttributeLogger.getLogger().info("******Test Rating of '"
13                 + getSimpleTestObjectClassName() + "'******");
14
15             try {
16                 // 从 settersMap 得到有多少组测试数据“ inputsNumber ”
17                 int inputsNumber = settersMap.getInputsNumber();
18                 // 对每组测试数据进行测试
19                 for (int i = 0; i < inputsNumber; i++) {
20                 // 把测试数据“ set ”到 Mock 对象中
21                 setInput(i);
22                 // 获取实际 Rating 值
23                 int rating = ((IRateable) testObject).getRating();
24                 // 比较实际 Rating 值和期望 Rating 值是否相等,得到测试结果
25                 assertEquals("Rating of '" + getSimpleTestObjectClassName()
26                     + "'", settersMap.getExpectedResult(i), rating + "");
27
28                 AttributeLogger.getLogger().info("Rating of '"
29                     + getSimpleTestObjectClassName() + "': "
30                     + rating+"(actual)/"+settersMap.getExpectedResult(i)+"(expected).");
31                 }
32             } catch (AdvisorException ae) {
33                 // TODO: find the handle method.
34                 ae.printStackTrace();
35             }
36         }
37
38     }
39
40 }
 编写 TestSuite 类

  由于我们构造了自己的 TestCase, TestSuite 常用的组织 TestCase 的方法需要做一点小小的改动。在我们的 TestSuite 中,提供了一个方法“ addTestCase ”。这个方法可以将 TestCase 添加到 TestSuite 中。参见清单 6。

  清单 6. addTestCase method

1 protected void addTestCase(IRateable testObject){
2     Test tp = null;
3     try{
4         // 获得 SettersMapFactory
5         Class factoryClass = Class.forName(factory);
6         SettersMapFactory settersMapFactory=
7             (SettersMapFactory)factoryClass.newInstance();
8             
9         // 从 SettersMapFactory 获得 SettersMap
10         File file = null;
11         file = getSetterResourceFile(testObject,settersMapFactory);
12         SettersMap settersMap = settersMapFactory.createSettersMap(file);
13             
14         // 创建 TestCase
15         tp = new AttributeRatingTestCase(testObject,settersMap);
16     }catch (Exception e){
17         e.printStackTrace();
18     }
19     // 添加 TestCase 到 TestSuite;
20     this.addTest(tp);        
21 }

  有了 addTestCase 方法 , 我们就可以轻易的把 TestCase 添加到 TestSuite 中了。参见清单 7。

  清单 7. LWIAttributesRatingTestSuite.java

1 public  LWIAttributesRatingTestSuite() {
2     // 获取 Logger.
3     AttributeLogger.getLogger();
4         
5     // 获取 SettersMapFactory 的名字
6     Properties confProps = new Properties();
7     try{
8         confProps.load(new FileInputStream(CONFIG_FILE));
9         factory = confProps.getProperty("SettersFactory");              
10     }catch(Exception e){
11         e.printStackTrace();
12     }
13         
14     // 添加 TestCase
15     System.out.println("LWIAttributesRatingTestSuite...");
16     addTestCase(new MockLWITracing());
17     addTestCase(new MockInitialHeapSize());
18     addTestCase(new PoolPreparedStatements(null));
19     addTestCase(new PoolMaxConnections(null));
20     addTestCase(new MaxOpenPreparedStatements(null));
21         
22 }
 如果你有很多的 TestSuite, 你应该把他们很好的组织起来。在我们的测试框架中, 一个 TestSuite 在其实例化阶段添加所有的 TestCase 。这就意味着我们只要拥有一个 TestSuite 的实例,我们就拥有了它所包含的 TestCase 。这样 , 一个 AllTest 类可以以如下方式来编写 :

  清单 8. AllTest.java

1 public class AllTest extends TestSuite{
2     
3     public AllTest(){
4         this.addTest(new LWIAttributesRatingTestSuite());
5         this.addTest(new LWIAttributesRecommendationTestSuite());
6     }
7     
8     public static void main(String[] args) {
9         AllTest at = new AllTest();
10         junit.textui.TestRunner.run(at);
11     }
12 }

  测试用例的组织可以用下图来说明。图中,每一个矩形都代表了一个“TestSuite”类。“TestSuite ”类以树形结构组织起来。你可以调用任何一个类的“main”方法来执行以这个类为树根的子树下的所有测试用例。以“WASAllTest”类为例,执行它的“main”方法将测试 “WASRecTestSuite”和 “WASRatingTestSuite”中的所有测试用例。

  图 3. 组织测试用例

  总结

  本文介绍了在对规则进行单元测试时实现可配置性和复用性。我们也介绍了一些常用的单元测试技术,比如使用 Mock 对象和扩展 JUnit 。这些技术可以使用到任何其他的单元测试中。

       代码下载地址

posted on 2009-03-06 10:04 rogerfan 阅读(380) 评论(0)  编辑  收藏 所属分类: 【Java知识】【开源技术】

只有注册用户登录后才能发表评论。


网站导航: