程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 擴展JUnit4以促進測試驅動開發

擴展JUnit4以促進測試驅動開發

編輯:關於JAVA

簡介:在采用測試驅動開發的項目中,有一個經常困擾開發者的問題是:當存在大量的測試用例時, 一次運行完所有的測試用例要花費很長的時間,采用 TestSuite 來組織測試用例的方式缺乏靈活性,通 常它的組織結構大體和 Java Package/Class 的組織結構類似,不能和當前實現的業務需求完全相關。本 文將通過擴展 JUnit4 來實現一種可以更加高效靈活地組織和運行測試用例的解決方案,促進測試驅動開 發實踐更有效地進行。

實際 Java 開發中單元測試常遇到的問題

在敏捷開發中,為了提高軟件開發的效率和質量,測試驅動開發實踐已經被廣泛使用。在測試驅動開 發的項目中,隨著項目開發不斷地深入,積累的測試用例會越來越多。測試驅動開發的一個最佳實踐是隨 時運行測試用例,保證任何時候測試用例都能成功執行,從而保證項目的代碼是可工作的。當測試用例數 量很多時,一次運行所有測試用例所消耗的時間可能會很長,導致運行測試用例的成本很高。所以在實際 敏捷開發中,如何組織、運行測試用例以促進測試驅動開發成為一個值得探究的問題。

JUnit 是 Java 開發中最常用的單元測試工具。在 JUnit3 用 TestSuite 來顯式地組織想要運行的 TestCase,通常 TestSuite 的組織大體上和 Java Package/Class 的組織類似,但這樣並不能和當前正 在實現的業務需求完全相關,顯得比較笨拙,比如說要運行某個子模塊下所有的 TestCase,或者運行跟 某個具體功能相關的 TestCase,涉及到的 TestCase 數量可能較多,采用定義 TestSuite 的方式一個個 地添加 TestCase 很低效並且繁瑣。在 JUnit4 中同樣只能顯式地組織要運行的 TestCase。

怎麼樣解決這些問題,新發布的 JUnit4 提供了開發人員擴展的機制,可以通過對 JUnit 進行擴展來 提供一種解決的方法。

JUnit4 的新特性和擴展機制

JUnit4 引入了 Java5 的 Annotation 機制,來簡化原有的使用方法。測試用例不再需要繼承 TestCase 類,TestSuite 類也取消了,改用 @Suite.SuiteClasses 來組織 TestCase。但是這種還是通 過顯示指定 TestCase 來組織運行的結構,不能解決上述的問題。關於 JUnit4 的新特性具體可以參考 developerworks 的文章。

JUnit4 的實現代碼中提供了 Runner 類來封裝測試用例的執行。它本身提供了 Runner 的多種實現, 比如 ParentRunner 類、Suite 類,BlockJUnit4ClassRunner 類。我們可以充分利用 JUnit4 提供的已 有設施來對它進行擴展,實現我們期望的功能。

首先我們來分析一下 JUnit4 在運行一個測試用例時,它內部的核心類是如何工作的。圖 1 展示了 JUnit4 運行測試用例時,核心類之間的調用關系。

圖 1. JUnit4 核心類之間的調用關系

在 JUnit4 中,Runner 類定義了運行測試用例的接口,默認提供的 Runner 實現類有 Suite、 BlockJUnit4ClassRunner、Parameterized 等等。Suite 類相當於 JUnit3 中的 TestSuite, BlockJUnit4ClassRunner 用來執行單個的測試用例。BlockJUnit4ClassRunner 關聯了一個 TestClass 類,TestClass 封裝了測試用例的 Class 元數據,可以訪問到測試用例的 method、annotation 等。 FrameworkMethod 封裝了測試用例方法的元數據。從下圖中我們可以看到這些類的關系。

圖 2. JUnit4 核心類

通過擴展 JUnit4,我們一方面可以無縫地利用 JUnit4 執行測試用例的能力,另一方面可以將我們定 義的一些業務功能添加到 JUnit 中來。我們將自定義一套與運行測試用例相關的業務屬性的 Annotation 庫,定義自己的過濾器,擴展 JUnit 類的 Runner,從而實現定制化的測試用例的執行。

JUnit4 擴展的實現

下面我們來描述一下對 JUnit4 擴展的實現。擴展包括 4 個模塊,Annotation 定義、用戶查詢條件 封裝、過濾器定義、核心類定義。

JUnit4 用 Annotation 來定義測試用例運行時的屬性。我們可以定義自己的 Annotation 庫。通過定 義出具體項目中和執行測試用例相關的屬性元數據, 比如某個模塊,某個特性,將這些屬性通過 Annotation 附加到測試用例中,在擴展的 Runner 中利用過濾器對測試用例進行過濾,從而執行目標測 試用例。

根據實際項目中的開發經驗,我們大體抽象出了如下的幾種 Annotation, 可以映射到我們項目的業務 功能劃分上;

表 1. 擴展的 Annotation 的具體用法

名稱 參數 作用域 Product 字符串參數,指定要測試的產品項目名稱 類 Release 字符串參數,指定具體的 Release 編號 類、方法 Component 字符串參數,指定子模塊、子系統 類 Feature 字符串參數,指定某個具體的功能、需求 類、方法 Defect 字符串參數,指定測試中發現的 Defect 的編號 類、方法 UseCaseID 字符串參數,指定 UseCase 的編號 類、方法

當我們想要運行所有和 Feature 相關的測試用例時,我們只要指定執行條件,就可以只運行那部分測 試用例,而不會去運行全部的測試用例。這種方法從業務的角度來看,更加具有針對性,而且簡潔快速, 比用傳統的通過 TestSuite 指定測試用例的方式更加適合測試驅動開發的場景。下面給出 Feature Annotation 和 Release Annotation 的定義作為示例。

清單 1:Feature Annotation 的定義

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Feature {
   String value();
}

清單 2:Release Annotation 的定義

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Release {
   String value();
}

接下來是封裝用戶輸入的執行條件。在這裡我們約定用戶輸入的執行條件的格式是:“條件 A = 值 A ,條件 B = 值 B”。比如用戶想執行 Release A 中的跟 Feature B 相關的測試用例和方法,那麼用戶 的輸入條件可以定義為“Release=A,Feature=B”。下圖是封裝用戶輸入的類的結構:

圖 3. 封裝用戶輸入的執行條件的類

過濾器是用來根據用戶輸入,對目標測試用例和測試方法進行過濾,從而找到符合條件的測試用例方 法。用戶輸入的每個條件都會生出相應的一個過濾器,只有測試用例滿足過濾器鏈中所有的過濾條件,測 試用例才能被執行。下面的清單展示了過濾器接口的定義和過濾器工廠的核心實現。過濾器工廠會根據用 戶輸入的條件來創建對應的過濾器。

清單 3 . Filter 接口的定義

public interface Filter {
   public boolean shouldRun(IntentionObject object);
}

清單 4 . FilterFactory 的部分實現

public class FilterFactory {
   public static Map<Class<?>, List<Filter>> createFilters (String intention)
   throws ClassNotFoundException{
     Map<Class<?>, List<Filter>> filters = new  HashMap<Class<?>, List<Filter>>();

     String[] splits = intention.split(ExtensionConstant.REGEX_COMMA);
     for(String split : splits){
       String[] pair = split.split(ExtensionConstant.REGEX_EQUAL);
       if(pair != null && pair.length == 2){
         Filter filter = createFilter(pair[0],pair[1]);
         String annotationType = ExtensionConstant.ANNOTATION_PREFIX + pair [0];
         Class<?> annotation = Class.forName(annotationType);
         List<Filter> filterList = null;
         if(filters.containsKey(annotation)){
           filterList = filters.get(annotation);
         }else{
           filterList = new ArrayList<Filter>();
         }
         filterList.add(filter);
         filters.put(annotation, filterList);
       }
     }
     return filters;
   }
   ………………
}

核心類模塊中的類是對 JUnit4 中的類的擴展,從下圖中可以看到兩者的繼承關系:

圖 4. 核心擴展類和 JUnit4 中類的繼承關系

Request 類是 JUnit4 中用來表示一次測試用例請求的抽象概念。它是一次測試用例執行的發起點。 RunerBuilder 會根據測試用例來創建相應的 Runner 實現類。BlockJUnit4ClassRunner 是 JUnit4 中用 來執行單獨一個測試用例的 Runner 實現類。我們通過擴展它,來獲得 JUnit 執行測試用例的能力,同 時在 ExtensionRunner 中調用過濾器對測試用例方法進行過濾,從而根據我們定義的業務規則來執行測 試用例。Result 類是 JUnit4 中用來封裝測試用例執行結果的類,我們對它進行了擴展,來格式化測試 用例執行結果的輸出。下面給出 ExtensionRunner 的部分實現。

清單 5. ExtensionRunner 部分實現

public class ExtensionRunner extends BlockJUnit4ClassRunner {

   private Map<Class<?>, List<Filter>>  filtersForAnnotation;

   public ExtensionRunner(Class<?> klass, String intention)
       throws InitializationError, ClassNotFoundException {
     super(klass);
     filtersForAnnotation = FilterFactory.createFilters(intention);
   }

   protected Statement childrenInvoker(final RunNotifier notifier) {
     return new Statement() {
       @Override
       public void evaluate() {
         runChildren(notifier);
       }
     };
   }

   protected void runChildren(final RunNotifier notifier) {
     for (final FrameworkMethod each : getFilteredChildren()) {
       runChild(each, notifier);
     }
   }

   protected List<FrameworkMethod> getFilteredChildren() {
   ArrayList<FrameworkMethod> filtered = new  ArrayList<FrameworkMethod>();
     for (FrameworkMethod each : getChildren()) {
       if (shouldRun(each)) {
         filtered.add(each);
       }
     }
     return filtered;
   }

   protected boolean shouldRun(FrameworkMethod method) {
     List<Boolean> result = new ArrayList<Boolean>();

     Annotation[] classAnnotations = method.getAnnotations();
     Map<Class<?>,Annotation> methodAnnotationMap =
       getAnnotaionTypeMap(classAnnotations);
     Set<Class<?>> annotationKeys = filtersForAnnotation.keySet ();
     for(Class<?> annotationKey : annotationKeys ){
       if(methodAnnotationMap.containsKey(annotationKey)){
         List<Filter> filters = filtersForAnnotation.get (annotationKey);
         if (filters != null) {
           for (Filter filter : filters) {
             if (filter != null
                 && filter.shouldRun(
                 IntentionFactory.createIntentionObject(
                 methodAnnotationMap.get(annotationKey)))) {
               result.add(true);
             }else{
               result.add(false);
             }
           }
         }

       }else{
         return false;
       }
     }

     if(result.contains(false)){
       return false;
     }else{
       return true;
     }

      ……………………
   }
}

通過測試用例實例展示 JUnit 擴展的執行效果

1)創建一個 Java 項目,添加對 JUnit4 擴展的引用。項目的結構如下:

圖 5. JUnit4 擴展示例程序的項目結構

2)創建一個簡單的待測試類 Demo 類。

清單 6. 待測試類

public class Demo {

   public int add(int a, int b){
     return a + b;
   }

   public int minus(int a, int b){
     return a - b;
   }
}

3)創建一個 JUnit4 風格的測試用例 DemoTest 類,對上述 Demo 類的方法編寫測試,並將我們自定 義的 Annotation 元數據嵌入到 DemoTest 的測試方法中。

清單 7. 包含了自定義 Annotation 的測試用例

public class DemoTest {

   @Test 
   @Feature("Test Add Feature")
   @Release("9.9")
   public void testAdd() {
     Demo d = new Demo();
     Assert.assertEquals(4, d.add(1, 2));
   }

   @Test 
   @Release("9.9")
   public void testMinus() {
     Demo d = new Demo();
     Assert.assertEquals(2, d.minus(2, 1));
   }
}

4)編寫 Main 類來執行測試用例,輸入自定義的執行測試用例的條件“Release=9.9,Feature=Test Add Feature”,來執行 9.9 Release 中跟 Add Feature 相關的測試用例方法,而不執行跟 Minus Feature 相關的測試用例方法。

清單 8. 調用 JUnit4 擴展來執行測試用例

public class Main {
   public static void main(String... args){
     new JUnitExtensionCore().runMain(args);
   }
}

圖 6. 自定義執行測試用例的條件

5) 執行結果:testAdd() 方法滿足執行的條件,它執行了。testMinus() 方法不滿足執行條件,它 沒有執行。

圖 7. 測試用例執行結果

6)改變自定義的執行條件為“Release=9.9”,執行跟 9.9 Release 相關的所有測試用例方法。

圖 8. 自定義執行測試用例的條件

7) 執行結果:testAdd() 方法和 testMinus() 方法都滿足執行條件,都執行了。

圖 9. 測試用例執行結果

結論

通過上述的代碼示例我們可以看出,我們通過對 JUnit4 進行擴展,從而可以自定義測試用例執行的 條件,將測試用例的執行和具體的業務功能結合在一起,快速地根據業務功能來執行相應的測試用例。這 種細粒度的,以業務屬性來組織測試用例的方法,更加適合以測試用例為本的測試驅動開發的需求。可以 實現快速地運行目標測試用例,從而促進測試驅動開發在項目中更好地實踐。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved