【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 。這些技術可以使用到任何其他的單元測試中。
代碼下載地址