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 閱讀(393) 評論(0)  編輯  收藏 所屬分類: 【Java知識】【開源技術】
          主站蜘蛛池模板: 白水县| 安达市| 普定县| 南安市| 应用必备| 太保市| 江川县| 遂昌县| 汪清县| 枣庄市| 泽普县| 巴马| 肇东市| 浮梁县| 盐城市| 商城县| 德州市| 和平县| 磐安县| 苏尼特右旗| 茂名市| 丽江市| 永新县| 尚志市| 凤庆县| 宿州市| 家居| 富宁县| 肥西县| 绥棱县| 平凉市| 葵青区| 望城县| 武强县| 井研县| 股票| 上虞市| 乳山市| 台山市| 华宁县| 丽江市|