天天看點

Jmockit使用指南概述Mock工具的原理:Jmockit的簡介:Jmockit的實踐:

概述

mock對象

虛拟的對象就是mock對象。mock對象就是真實對象在調試期間的代替品。

關于什麼時候需要Mock對象,Tim Mackinnon給我們了一些建議:

真實對象具有不可确定的行為(産生不可預測的結果,如股票的行情)

真實對象很難被建立(比如具體的web容器)

真實對象的某些行為很難觸發(比如網絡錯誤)

真實情況令程式的運作速度很慢

真實對象有使用者界面

 測試需要詢問真實對象它是如何被調用的(比如測試可能需要驗證某個回調函數是否被調用了)

真實對象實際上并不存在(當需要和其他開發小組,或者新的硬體系統打交道的時候,這是一個普遍的問題)

比如以下場景:

1. mock掉外部依賴的應用的HSF service的調用,比如uic,tp 的hsf服務依賴。

2. 對DAO層(通路mysql、oracle、tair、tfs等底層存儲)的調用mock等。

3. 對系統間異步互動notify消息的mock。

4. 對method_A裡面調用到的method_B 的mock 。

5. 對一些應用裡面自己的 class(abstract, final, static),interface,annotation ,enum,native等的mock。

Mock工具的原理:

1. record階段:錄制期望。也可以了解為資料準備階段。建立依賴的class 或interface或method ,模拟傳回的資料,及調用的次數等。

2. replay階段:通過調用被測代碼,執行測試。期間會invoke 到 第一階段record的mock對象或方法。

3. verify階段:驗證。可以驗證調用傳回是否正确。及mock的方法調用次數,順序等。

利用JMockit工具來編寫基于行為的測試代碼,通常符合下面的經典模闆:

import mockit.*;  
... other imports ...  
   
public class SomeTest  
{  
   //  零個或者更多的mock 屬性, 這些屬性對于整個類的所有測試方法來說是通用的。  
   @Mocked Collaborator mockCollaborator;  
   @NonStrict AnotherDependency anotherDependency;  
   ...  
   
   @Test  
   public void testWithRecordAndReplayOnly(mock parameters)  
   {  
      // 如果這裡需要測試前的準備,可以在這裡執行,但對于Jmockit 來說,對此沒特别要求。當然這裡也可以為空。  
   
      new Expectations() // 一個期望塊  
      {  
         // 零個或者多個局部 mock 屬性域  
   
         {  
            //  一個或者多個mock對象(類型)的調用,這些調用會被Expectations記錄(Recorded)下來  
            //一些沒有被mock的方法、對象類型等同樣可以在這個期望塊裡面調用  
         }  
      };  
   
      // 單元測試代碼真正業務邏輯在此執行  
   
      // 如果需要,可以在這裡進行驗證代碼編寫,當然可以利用JUnit/TestNG 斷言  
   }  
   
   @Test  
   public void testWithReplayAndVerifyOnly(mock parameters)  
   {  
      // 如果這裡需要測試前的準備,可以在這裡執行,但對于Jmockit 來說,對此沒特别要求。當然這裡也可以為空。  
   
      // 單元測試代碼真正業務邏輯在此執行  
   
      new Verifications() {{ // 一個驗證塊  
          //  一個或者多個mock對象(類型)的調用,這些調用用于驗證結果是否正确  
            //一些沒有被mock的方法、對象類型等同樣可以在這個驗證塊裡面調用  
      }};  
   
      // 如果需求,這裡可以添加其他額外的驗證代碼,  
     //  當然,這些驗證可以編寫在這裡,也可以在Verifications塊之前  
   }  
   
   @Test  
   public void testWithBothRecordAndVerify(mock parameters)  
   {  
      //如果這裡需要測試前的準備,可以在這裡執行,但對于Jmockit 來說,對此沒特别要求。當然這裡也可以為空。  
   
      new NonStrictExpectations() { // 同樣是一個期望塊  
         //零個或者多個局部 mock 屬性域  
         {  
            // 一個或者多個mock對象(類型)的調用,這些調用會被Expectations記錄(Recorded)下來  
         }  
      };  
   
      // 單元測試代碼真正業務邏輯在此執行  
   
      new VerificationsInOrder() {{ // 同樣是一個驗證塊  
         // 一個或者多個mock對象(類型)的調用,這些調用将期望按照特定的順序進行比較。  
      }};  
   
     // 如果需求,這裡可以添加其他額外的驗證代碼,  
     //  當然,這些驗證可以編寫在這裡,也可以在Verifications塊之前  
   }  
           

Jmockit的簡介:

 Jmockit可以mock的種類包含了:

        1.class(abstract, final, static) ;

        2.interface ;

        3.enum ;

        4.annotation ;

        5.native 。

 Jmockit 兩種mock的方式:

一.根據用例的測試路徑,測試代碼内部邏輯Behavior-oriented(Expectations & Verifications) 

        對于這種情景,可以使用jmockit的基于行為的mock方式。目的是從被測代碼的使用角度出發,結合資料的輸入輸出來檢驗程式運作的這個正确性。使用這個方式,需要把被依賴的代碼mock掉,實際上相當于改變了被依賴的代碼的邏輯。通常在內建測試中,如果有難以調用的外部接口,就通過這個方式mock掉,模拟外部接口。 這種方式有點像黑盒測試。

二.根據測試用例的輸入輸出資料,測試代碼是否功能運作正常。State-oriented(MockUp<GenericType>)  

        對于這種情景,可以使用jmockit基于狀态的mock方式。在這種方式中,目的是測試單元測試及其依賴代碼的調用過程,驗證代碼邏輯是否滿足測試路徑。  由于被依賴代碼可能在自己單測中已測試過,或者難以測試,就需要把這些被依賴代碼的邏輯用預定期待的行為替換掉,也就是mock掉,進而把待測是代碼隔離開,這也是單元測試的初衷。 這種方式和白盒測試接近。

通俗點講,Behavior-oriented是基于行為的mock,對mock目标代碼的行為進行模仿,更像黑盒測試。State-oriented 是基于狀态的mock,是站在目标測試代碼内部的。可以對傳入的參數進行檢查、比對,才傳回某些結果,類似白盒。而State-oriented的 new MockUp基本上可以mock任何代碼或邏輯。非常強大。

JMockit元素

@Tested和@Injectable: 

對@Tested對象判斷是否為null,是則通過合适構造器初始化,并實作依賴注入。調用構造方法時,會嘗試使用@Injectable的字段進行構造器注入。普通注入時,@Injectable字段如果沒有在測試方法前被指派,其行為将會被mock成預設值(靜态方法和構造函數不會被mock掉)。Injectable最大作用除了注入,還有就是mock的範圍隻限目前注釋執行個體。一句話:@Injectable的執行個體會自動注入到@Tested中,如果沒初始指派,那麼JMockit将會以相應規則初始化。

@Mocked:

@Mocked修飾的執行個體,将會把執行個體對應類的所有執行個體的所有行為都mock掉(無論構造方法,還是private,protected方法,夠霸氣吧)。在Expectation區塊中,聲明的成員變量均預設帶有@Mocked,@Mocked會mock掉所有方法,如果某些函數我們不希望它也被mock,可以通過methods="methodName"來設定被mock的類隻對methodName方法進行mock。或者通過Expectation構造函數傳入Class對象或Instance對象,這樣隻會區塊内Class或Instance對應的行為進行mock。

比如:

@Test  
    public void behaviorTest_fail3time() {  
               
        new Expectations() {        // Expectations中包含的内部類區塊中,展現的是一個錄制被測類的邏輯。  
            @Mocked(methods="tryIt")  // 表明被修飾的類對tryIt()方法進行mock。  
            Guess g;  
            {  
                g.tryIt();             // 期待調用Guess.tryIt()方法  
                result = false;        // mock掉傳回值為false(表明猜不中)  
                times = 3;             // 期待以上過程重複3次  
                guessDAO.saveResult(false, anyInt); // 期待調用guessDAO把猜失敗的結果儲存  
            }  
        };  
        guess.doit();               // 錄制完成後,進行實際的代碼調用,也稱回放(replay)  
    }  
}  
           
public class Test {
  @Mocked
  private Flow flow; 
  @Tested
  private CServiceImpl cService = new CServiceImpl();
  @org.junit.Test
  public void test(@Mocked Flow flow) {
    new  Expectations(CnServiceImpl.class){
      {
         System.out.println(cService.getBundleId());
      }
    };
  }
}
           

在Expectation中沒定義成員變量,而把CnServiceImpl.class顯式地通過構造函數傳入。這麼做也是為了隻對getBundleId方法mock,因為在Expectation構造函數傳入Class對象或Instance對象後,隻會區塊内Class或Instance對應的行為進行mock。

1、字段,期望塊的字段與期望塊内的局部屬性字段使用@Mocked來聲明Mock類型。

2、參數,方法的參數聲明來引入一個Mock類型。

第一種情況,屬性字段是屬于測試類或者一個mockit.Expectations子類(一個expectation期望塊的内部的局部屬性字段)。

第二種情況,參數必須是屬于某個測試方法(@Test标簽下的方法)。

在所有的情況,一個mock屬性字段或者參數聲明,都可以通過使用@Mocked聲明。對于方法mock的參數或者在expectations期望塊中定義的mock屬性字段來說,該注解是可選的,而對于定義在測試類(XXXTest類)中的屬性字段,@Mocked标簽是必須,這是為了防止和該測試類的其它不需要mock的字段屬性産生沖突。

Expectations:

Expectations是一個給定的單元測試相關的mock方法/構造函數的調用集合,是錄制期望發生行為的地方。對于一個同樣的方法或者構造函數,一個Expectations 可能會覆寫到多個不同調用,但是它不需要(不一定)覆寫到單元測試方法執行期間的所有調用(invocations)。一個特定的調用是否比對給定的 expectation,不僅依賴方法/構造函數的簽名,而且依賴運作時方面參數(aspects),例如被調用的方法類執行個體、參數值以及調用次數等等。 是以,對于給定的expectation,可以(可選)指定幾種不同類型的比對限制。result和times都是其内定成員變量。result可以重定義錄制行為的傳回值甚至通過Delegate來重定義行為,times是期待錄制行為的發生次數。在Expectations中發生的調用,均會被mock。如果沒定義result,方法調用的結果傳回空。

對于一個傳回值不為void類型的方法,Expectations中如何模拟方法傳回值:

1)其傳回值可以通過Expectations的result屬性域來記錄

2)Expectations的returns(Object)方法來記錄

例如,方法傳回一個Throwable異常類,隻需将一個類型實驗賦給result(注意,異常類隻能通過result方式指派,但是,在一些不常見的情況下面,有一個方法它實際就是傳回一個異常或者錯誤對象時,我們就需要使用 returns(Object)方法來防止産生二義性。請注意,被抛出的異常/錯誤的記錄,是适用于mock的方法(包括任何傳回類型),以及mock的 構造函數。)。 

JMockit也可以分類為非局部模拟與局部模拟,區分在于Expectations塊是否有參數,有參數的是局部模拟,反之是非局部模拟。

而Expectations塊一般由Expectations類和NonStrictExpectations類定義,類似于EasyMock和PowerMock中的Strict Mock和一般性Mock。

用Expectations類定義的,則mock對象在運作時隻能按照 Expectations塊中定義的順序依次調用方法,不能多調用也不能少調用,是以可以省略掉Verifications塊;

而用NonStrictExpectations類定義的,則沒有這些限制,是以如果需要驗證,則要添加Verifications塊。

當這個方法在重播階段調用時,這個被記錄下來的特定的值将傳回給調用者(通常情況下,這個調用者就是測試代碼)。但 是,必須保證的是,在一個expectation期望塊中,result的指派或者returns(Object) 方在同一個expectation期望中,可以通過簡單是對result屬性域進行指派,進而記錄多個連續的結果(結果值包括傳回值和抛出來的 throwable執行個體)。

多個傳回值或者異常錯誤在記錄階段可以混合使用。對于記錄多個連續的傳回值的情況,形似returns(Object, Object...)這樣的方法調用就可以滿足了。同樣,如果将一個包含了多個連續的值的清單list或者資料array指派給result屬性域,使用 一個result屬性域也可以達到相同的效果。更多細節,可以參考相應的API文檔

下面的例子展示了這樣的情況:在UnitUnderTest記錄階段,對mock類DependencyAbc的方法同時記錄了兩種類型的傳回結果。實作如下所示:法的調用,必須僅靠在記錄階段的方法調用所在地方的後面。

public class UnitUnderTest
{
(1)private final DependencyAbc abc = new DependencyAbc();
 
   public void doSomething()
   {
(2)   int n = abc.intReturningMethod();
 
      for (int i = 0; i < n; i++) {
         String s;
 
         try {
(3)         s = abc.stringReturningMethod();
         }
         catch (SomeCheckedException e) {
            // 處理異常
         }
 
         // 這裡可以處理其他邏輯
      }
   }
}
           

對于方法doSomething() 來說,一種可能的執行結果是在幾個循環成功執行後,抛出SomeCheckedException異常。假設我們需要記錄一個完整的期望集合(無論處于什 麼原因),我們可能像下面這樣編寫測試代碼。(通常情況下,對于mock的方法沒有必要去指定所有可能的調用(invocations),也是不重要的, 特别是對于mock構造函數。

@Test
   public void doSomethingHandlesSomeCheckedException() throws Exception
   {
      new Expectations() {
         DependencyAbc abc;
 
         {
(1)         new DependencyAbc();
 
(2)         abc.intReturningMethod(); result = 3;
 
(3)         abc.stringReturningMethod();
            returns("str1", "str2");
            result = new SomeCheckedException();
         }
      };
 
      new UnitUnderTest().doSomething();
   }
           

這裡記錄了三種不同的期望值。第一個(其實就是 DependencyAbc() 的構造函數調用)實際上會在測試代碼中通過一個無參的構造函數來初始化這些依賴,對于這種調用是不需要任何傳回值的,除非在構造函數裡面抛出一個異常或者 錯誤(其實構造函數是沒有傳回值的,是以對它來說記錄傳回值是沒什麼意義可說)。第二個期望指定調用intReturningMethod()後将傳回值 3。第三個期望就是,調用stringReturningMethod()方法後将按順序傳回3個連續的期望值,注意下,最後一個結果其實是一個需要檢查 的異常執行個體,這樣允許測試代碼去到達它最初的目的

可伸縮的參數值比對

在記錄和驗證階段,一個mock方法或者構造函數的調用标示一個expectation期望。如果方法/構造函數具有一個或者多個參數,那麼一個記錄/驗 證的expectation期望格式類似:doSomething(1, "s", true);如果在重播階段存在一個調用,它具有相同(equal)的參數值,那麼這次調用将比對該期望。對于一般的對象(不是原生的對象或者數組),它 的 equals(Object) 方法将用在相等性的檢查。對于數組類型參數,相等性取決于各個獨立的元素的相等性。是以,兩個不同的數組執行個體,在每一維(譯者注:數組可能存在多元數組的 情況)必須具有相同的長度,而且按順序比較各個元素的相等(利用equals(Object)方法),才能認為兩個數組執行個體相等。

給定一個測試用例,我們通常是不知道這些參數值到底是什麼,或者這些參數對于測試的單元并不是必須的。是以,我們可以通過指定一個具有伸縮性(或者 說是靈活吧)參數比對限制,而不是使用精确的參數值比對限制,進而允許測試代碼在重播階段通過不同的參數值也可以比對Record或者Verified階 段聲明的 期望調用集合。這個功能是通過使用withXyz(...)方法和 (或者)anyXyz域來實作。這些帶有字首"with"的方法 和字首"any"的域都是定義在基類 mockit.Invocations裡面。這個基類是mockit.Expectations和 mockit.Verifications的父類。是以,這些方法和域可以同時在Expectations和Verifications塊中使用。

使用"with' 方法比對參數

當錄制或者校驗一個期望時,調用withXyz(...)方法可以在産生任意的參數子集,這些參數是通過調用進行傳遞。它們和正常的參數傳遞(使用具體 值、局部變量等)可以一起自由混合使用。唯一需要滿足的是,這些調用必須出現錄制/校驗調用語句裡面,而不是前面。這是不可能的,例如,先配置設定 withNotEqual(VAL)的調用結果到一個局部變量,然後在調用語句中使用這個變量。下面是一個測試例子,使用一些"with"的方法。 

當錄制或者校驗一個期望時,調用withXyz(...)方法可以在産生任意的參數子集,這些參數是通過調用進行傳遞。它們和正常的參數傳遞(使用具體 值、局部變量等)可以一起自由混合使用。唯一需要滿足的是,這些調用必須出現錄制/校驗調用語句裡面,而不是前面。這是不可能的,例如,先配置設定 withNotEqual(VAL)的調用結果到一個局部變量,然後在調用語句中使用這個變量。下面是一個測試例子,使用一些"with"的方法。 

@Test
   public void someTestMethod(@NonStrict final DependencyAbc abc)
   {
      final DataItem item = new DataItem(...);
 
      new Expectations() {{
         // 那些第一個參數等于"str"而且第二個參數不為null的調用将比對"voidMethod(String, List)"方法.
         abc.voidMethod("str", (List<?>) withNotNull());
 
         //對于類 DependencyAbc的執行個體,如果調用的stringReturningMethod(DataItem, String)的方法,
        //滿足第一個參數指針指向同一個"item",而且第二個參數俺有字元串 "xyz".那麼該次調用将比對下面的期望
         abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
      }};
 
      new UnitUnderTest().doSomething(item);
 
      new Verifications() {{
         // 比對所有參數為任何long類型的方法調用.
         abc.anotherVoidMethod(withAny(1L));
      }};
   }
           

除了幾個預定義的參數比對的限制API,JMockit允許使用者通過<T> T with(Object) and <T> T with(T, Object>) 這樣的泛型方法來提供自定義的限制。參數Object類型可以是org.hamcrest.Matcher 對象(Hamcrest 庫的對象),或者是一個合适的句柄對象

使用"any"屬性字段比對參數

最常見的參數比對限制往往就是限制條件最少的那種比對限制:比對任何一個給定的參數(當然是正确的參數類型)的值調用。為此,我們有一個參數比對的 特殊的屬性字段集合,一些是為比對所有的原始類型(包括相應的包裝類),一些是用于字元串,以及一些用于比對"通用"類型的對象。下面的測試示範了一些使 用。

@Test
   public void someTestMethod(@NonStrict final DependencyAbc abc)
   {
      final DataItem item = new DataItem(...);
 
      new Expectations() {{
         // 這裡期望的聲明,會比對所有這樣的"voidMethod(String, List)"方法調用:
        // 其第一個參數為任意字元串,而且第二個參數是任何一個list執行個體對象
         abc.voidMethod(anyString, (List<?>) any);
      }};
 
      new UnitUnderTest().doSomething(item);
 
      new Verifications() {{
         // 比對參數類型是long或者Long的方法調用
         abc.anotherVoidMethod(anyLong);
      }};
   }
           

當一個或者多個參數比對同時使用時,而且對于給定的參數必須比對null引用,那麼withNull()就應該被使用。

總之,這裡有兩個參數比對模式:一個基本的比對是,沒有任何的比對限制指定所有參數必須是相等的;而另一個比對是,存在一個比對指定部分或全部參數 對應一個比對的限制。但null值和上面的每個模式都不太一樣,這可能會導緻混亂。不過,對于涉及多個參數的複雜調用,可以使用"any"屬性字段和 null引用的好處大于附加在API上的複雜性。

通過一個"可變參數(varargs )"參數來比對值

有時,我們可能需要處理帶有"可變參數"的方法或構造函數的期望。通過傳遞一個正常值來作為一個可變參數值是有效的,同樣,使 用"with"、"any"比對器來比對也是有效的。然而,對于一個結合了兩種值傳遞的相同期望,這并不是有效的。我們要麼隻能使用正常值或者參數比對 值。

這種情況下,我們要比對可變參數接收任何值(包括零)的調用,對這樣的可變參數,我們可以指定一個期望使用個(Object[])any的限制來進行比對。

也許最好的方式來了解可變參數比對的确切語義(因為沒有涉及特定的API)是閱讀或實踐實際的測試代碼。這個 測試類 示範了幾乎所有的可能性。

使用null值比對任意對象引用

對于給定的一個期望,當我們需要使用至少一個參數比對方法或者字段時,我們可以使用一個 "便捷"的方式去指定該期望接受所有任意的對象引用(引用類型的參數),隻需使用一個null值,而不是使用withAny(X)或"any'屬性字段, 特别是,這樣可以不需要将值的類型轉換為參數聲明時的類型。然而,需要記住的是,這種行為隻适合這樣的期望,它使用了至少一個顯式的參數比對(或 是"with"方法,或是"any"屬性字段)。當null值傳遞給一個沒有任何比對的調用時,空值将隻比對空引用。在前面的測試,是以我們可以這樣寫:

@Test
   public void someTestMethod(@NonStrict final DependencyAbc abc)
   {
      ...
 
      new Expectations() {{
         abc.voidMethod(anyString, null);
      }};
 
      ...
   }
           

到目前為止,我們可以看出,一個expectation除了可以關聯一個方法或者一個構造函數,還是可以指定調用的傳回結果和參數比對限制。在下面 這種情況下:在單元測試代碼中,需要多次調用同一個方法或者構造函數,但其參數是不同的,此時,我們需要一種方法去聲明期望去滿足這些互相獨立的調用。一 種方式是,就好像之前所見的,就是簡單的為每一個調用聲明一個獨立的期望,聲明順序保持和調用執行時的順序。另一種方式,對單個expectation期 望聲明記錄下兩個或者更多個連續的傳回結果。

而然,還存在另外一種方式,就是對一個給定的期望,指定該期望對應的調用執行次數的限制。為此,jmockit提供了三個特定的屬性字段 域:times, minTimes和maxTimes。這些屬性字段是屬于mockit.Invocations類的,它是mockit.Expectations和 mockit.Verifications的一個非public的基類。是以,一個調用次數的限制可以用于記錄階段和檢驗階段。在這幾種情況下,與期望相 關聯的方法或構造,在指定範圍内将受到指定的調用次數的限制。一個調用如何少于期望的最少執行次數,或者超過期望的執行次數的上限,這時,單元測試會自動 失敗。讓我們看下面一些例子:

@Test
   public void someTestMethod(final DependencyAbc abc)
   {
      new Expectations() {{
         // By default, one invocation is expected, i.e. "times = 1":
         new DependencyAbc();
 
         // At least two invocations are expected:
         abc.voidMethod(); minTimes = 2;
 
         // 1 to 5 invocations are expected:
         abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
      }};
 
      new UnitUnderTest().doSomething();
   }
 
   @Test
   public void someOtherTestMethod(final DependencyAbc abc)
   {
      new UnitUnderTest().doSomething();
 
      new Verifications() {{
         // Verifies that zero or one invocations occurred, with the specified argument value:
         abc.anotherVoidMethod(3); maxTimes = 1;
 
         // Verifies the occurrence of at least one invocation with the specified arguments:
         DependencyAbc.someStaticMethod("test", false); // "minTimes = 1" is implied
      }};
   }
           

但不同于result屬性字段的是,對于一個給定的期望,這三個屬性字段最多隻可以被指定一次。對于任何的調用次數的限制,一個非負整數都是有效 的。如果times=0或者maxTimes=0,那麼在重播階段(如果存在),發現存在一個調用能比對上期望,則測試用例會是以失敗。

Jmockit的實踐:

第一步:添加jmockit的jar包依賴 

<dependency>  
            <groupId>com.googlecode.jmockit</groupId>  
            <artifactId>jmockit</artifactId>  
            <version>1.5</version>  
            <scope>test</scope>  
        </dependency>  
        <dependency>  
            <groupId>com.googlecode.jmockit</groupId>  
            <artifactId>jmockit-coverage</artifactId>  
            <version>0.999.24</version>  
            <scope>test</scope>  
        </dependency>  
           

現在假設我們有一個服務類能提供一下幾種服務:1:查詢資料庫中的資料2:儲存建立的對象到資料庫中3:發送一封郵件到訂閱者。前兩個功能需要通過一個第三方的api庫通路一個資料庫。 第三個服務同樣需要一個第三方的庫來操作email程式。本例子中使用的是commons-email

是以這個例子中依賴了兩個第三方的庫,為了能能對這個服務進行單元測試我們需要通過一些Mock的api來模拟第三方的互動。 

package jmockit.tutorial.domain;

import java.math.*;
import java.util.*;
import org.apache.commons.mail.*;
import static jmockit.tutorial.persistence.Database.*;

public final class MyBusinessService
{
   public void doBusinessOperationXyz(EntityX data) throws EmailException
   {
      List<EntityX> items =
(1)      find("select item from EntityX item where item.someProperty = ?1", data.getSomeProperty());

      // Compute or obtain from another service a total value for the new persistent entity:
      BigDecimal total = ...
      data.setTotal(total);

(2)   persist(data);

      sendNotificationEmail(data, items);
   }

   private void sendNotificationEmail(EntityX data, List<EntityX> items) throws EmailException
   {
      Email email = new SimpleEmail();
      email.setSubject("Notification about processing of ...");
(3)   email.addTo(data.getCustomerEmail());

      // Other e-mail parameters, such as the host name of the mail server, have defaults defined
      // through external configuration.

      String message = buildNotificationMessage(items);
      email.setMsg(message);

(4)   email.send();
   }

   private String buildNotificationMessage(List<EntityX> items) { ... }
}
           

資料庫類隻包含靜态方法和私有構造函數;查找和儲存的方法我們不會在這裡列出他們(假設他們是一個ORM API實作,如Hibernate或JPA)。

那麼,如何才能不做任何改變現有的應用程式對“doBusinessOperationXyz”的方法進行單元測試? JMockit實際上提供了兩種不同的Mock的API,每個有足夠的強大能滿足我們的需要。 (這個我們在前面有談到,一個是行為導向Mock的API;另外一個,它可以被描述為一個面向狀态的Mock的API)我們将在下面的看到如何使用他們。一個JUnit測試用例将驗證從單元測試向它的外部依賴利息調用。這些調用的是那些在點(1) - (4)的上方。

使用Expectations API

首先,讓我們先來看看JMockit Expectations API.

package jmockit.tutorial.domain;

import org.apache.commons.mail.*;
import jmockit.tutorial.persistence.*;

import org.junit.*;
import mockit.*;

public final class MyBusinessService_ExpectationsAPI_Test
{
   @Mocked(stubOutClassInitialization = true) final Database unused = null;
   @Mocked SimpleEmail email;

   @Test
   public void doBusinessOperationXyz() throws Exception
   {
      final EntityX data = new EntityX(5, "abc", "[email protected]");

      // Recorded strictly, so matching invocations must be replayed in the same order:
      new Expectations() {{
(1)      Database.find(withSubstring("select"), any);
         result = new EntityX(1, "AX5", "[email protected]");

(2)      Database.persist(data);
      }};

      // Recorded non-strictly, so matching invocations can be replayed in any order:
      new NonStrictExpectations() {{
(4)      email.send(); times = 1; // a non-strict invocation requires a constraint if expected
      }};

      new MyBusinessService().doBusinessOperationXyz(data);
   }

   @Test(expected = EmailException.class)
   public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception
   {
      new NonStrictExpectations() {{
(3)      email.addTo((String) withNotNull()); result = new EmailException();

         // If the e-mail address is invalid, sending the message should not be attempted:
         email.send(); times = 0;
      }};

      EntityX data = new EntityX(5, "abc", "[email protected]");
      new MyBusinessService().doBusinessOperationXyz(data);
   }
}
           

首先,先看下哪些字段可以被Mocked來修飾,本例中是測試類的執行個體字段标注為@Mocked。Mock字段可以是任何引用類型:一個接口,一個抽象類,final/ final類,enum類型、注釋類型,甚至是一個泛型類型參數。我們所說的一個被模拟的域有一個mocke類型,其實就是指定的類型(比如@Mocked或者@Injection,@Tested等),一個mock 屬性字段或者參數聲明,都可以通過使用 mockit.Mocked注解(@Mocked)聲明。對于方法mock的參數或者 在expectation 期望塊中定義的mock屬性字段來說,該注解是可選的。注解@Mocked(或者其他mock的注解類型,例如 @NonStrict)隻是對于定義在測試類中的屬性字段域才是必須的,這是為了防止和該測試類的其他不需要mock的字段屬性産生沖突而已。

對于聲明在測試方法的參數清單中的mock參數,當調用執行該測試方法時,Jmockit會對該聲明類型的參數自動建立一個執行個體,并通過JUnit/TestNG 測試架構進行傳遞。是以這個參數值永遠不會為null的。

對于mock屬性字段域,Jmockit同樣會自動建立一個執行個體,并設定到該屬性字段域中,除非該字段域是一個final的域。對于這種情況,需要 在測試代碼中顯式的建立一個值并設定到域中。如果隻有構造函數和靜态方法将要調用它,那麼這個域的值可以是null的,這樣對于 mock的類來說也是有效的。

預設情況下,被mock的類型的所有方法在測試期間都被 mock實作,構造函數也是一樣的。類初始化代碼(在靜态塊和/或non-compile時間配置設定靜态字段)會被取消,正如上面的資料庫類。如果一個mock類型被聲明為類,那麼除了java.lang.Object之外,該類的父類将被遞歸mock。是以,繼承的方法也将自動 被mock。同樣,對于聲明為類的mock類型,其所有構造函數也将被 mock。甚至,無論方法或者構造函數的修飾符是否是private,stati,final,native等,這些方法和構造函數都會被mock掉,對 于mock類型來說,修飾符的定義變得如此不重要了。

在一個測試調用中,如果當一個方法或者構造函數被mock了,則其原始的實作代碼将不會被執行,取而代之的是,可以通過 jmockit顯式或者隐式指定測試調用。

(注意JMockit不建立任何要模拟對象的子類;它直接修改mock類的實際實作。換句話說,一個接口方法和抽象方法可以不需要有任何實作代碼。)

使用Verifications API

前面我們說的都是模拟對象和錄制行為,接下來我們來看看如何校驗非嚴格的執行結果

package jmockit.tutorial.domain;

import org.apache.commons.mail.*;
import jmockit.tutorial.persistence.*;

import org.junit.*;
import mockit.*;

public final class MyBusinessService_VerificationsAPI_Test
{
   @Tested MyBusinessService service; // instantiated automatically
   @Mocked(stubOutClassInitialization = true) Database onlyStatics;
   @Capturing Email email; // concrete subclass mocked on demand, when loaded

   final EntityX data = new EntityX(5, "abc", "[email protected]");

   @Test
   public void doBusinessOperationXyzPersistsData() throws Exception
   {
      // No expectations recorded in this case.

      service.doBusinessOperationXyz(data);

(2)   new Verifications() {{ Database.persist(data); }};
   }

   @Test
   public void doBusinessOperationXyzFindsItemsAndSendsNotificationEmail() throws Exception
   {
      // Invocations that produce a result are recorded, but only those we care about.
      new NonStrictExpectations() {{
(1)      Database.find(withSubstring("select"), (Object[]) null);
         result = new EntityX(1, "AX5", "[email protected]");
      }};

      service.doBusinessOperationXyz(data);

      new VerificationsInOrder() {{
(3)      email.addTo(data.getCustomerEmail());
(4)      email.send();
      }};
   }
}
           

Expectations塊一般由Expectations類和NonStrictExpectations類定義,類似于EasyMock和PowerMock中的Strict Mock和一般性Mock。

用Expectations類定義的,則mock對象在運作時隻能按照 Expectations塊中定義的順序依次調用方法,不能多調用也不能少調用,是以可以省略掉Verifications塊;而用NonStrictExpectations類定義的,則沒有這些限制,是以如果需要驗證,則要添加Verifications塊。

在上面的例子中,對于第三方的封裝的類進行mock之後,在Expectations體中錄制方法調用的行為和結果,然後在調用要測試的方法,例子中用到了NonStrictExpectations和Expectations兩種期望,第一種是非嚴格的,裡面錄制的代碼不一定都執行,你可以在調用完測試方法後中的Verifications進行校驗調用次數,而Expectations則預設自動校驗。當我們使用的非嚴格的期望,在回放階段調用mock方法和構造函數不會立即驗證(除非顯式指定否則通過調用數限制)。那些記錄的非嚴格調用與一個特定的傳回值或/錯誤抛出一個異常會産生預期的結果如果重播生産代碼。

非嚴格期望隻是證明測試單元做了正确的事情。例如,在上面的第二個測試假設記錄Database.find(…)的行調用被注釋掉了。測試會失敗,另一部分的代碼在測試取決于執行傳回值時,或者當一個預期調用驗證測試本身(在這個例子測試中,一個額外的email.setMsg(withNotEqual(" ")))在需要驗證的其他兩個驗證調用之間),在某些情況下,您可能希望確定調用至少會發生一次。可以簡單地指定它minTimes = 1限制非嚴格記錄調用後的期望。

Using the Mockups API

package jmockit.tutorial.domain;

import java.util.*;
import org.apache.commons.mail.*;
import jmockit.tutorial.persistence.*;

import static org.junit.Assert.*;
import org.junit.*;
import mockit.*;

public final class MyBusinessService_MockupsAPI_Test
{
   public static final class MockDatabase extends MockUp<Database>
   {
      @Mock
      public void $clinit() { /* do nothing */ }

      @Mock(invocations = 1)
(1)   public List<EntityX> find(String ql, Object... args)
      {
         assertNotNull(ql);
         assertTrue(args.length > 0);
         return Arrays.asList(new EntityX(1, "AX5", "[email protected]"));
      }
   
      @Mock(maxInvocations = 1)
(2)   public void persist(Object o) { assertNotNull(o); }
   }

   @BeforeClass
   public static void mockUpPersistenceFacade()
   {
      // Applies the mock class by invoking its constructor:
      new MockDatabase();
   }

   final EntityX data = new EntityX(5, "abc", "5453-1");

   @Test
   public void doBusinessOperationXyz() throws Exception
   {
      // Defines and applies a mock class in one operation:
      new MockUp<Email>() {
         @Mock(invocations = 1)
         Email addTo(Invocation inv, String email)
         {
            assertEquals(data.getCustomerEmail(), email);
            return inv.getInvokedInstance();
         }

         @Mock(invocations = 1)
(4)      String send() { return ""; }
      };
   
      new MyBusinessService().doBusinessOperationXyz(data);
   }

   @Test(expected = EmailException.class)
   public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception
   {
      new MockUp<Email>() {
         @Mock
(3)      Email addTo(String email) throws EmailException
         {
            assertNotNull(email);
            throw new EmailException();
         }
      
         @Mock(invocations = 0)
         String send() { return null; }
      };
   
      new MyBusinessService().doBusinessOperationXyz(data);
   }
}
           

上面的例子中,已經不是通過調用mocked類型記錄或驗證預期,我們直接指定感興趣的mock的實作方法和構造函數。待模拟方法必須具有相同的方法和構造函數,并用@Mock注釋。他們是定義在一個模拟類中,它可以是在一個定義測試方法的一個單獨的類(嵌套)或一個匿名内部類;在這兩種情況下,它必須擴充通用模型< T >基類,同時提供類型被mock為類型參數T的“value”。

上面的兩個測試共享一個可重用的模拟類,MockDatabase,應用到測試類作為一個整體在一個@BeforeClass方法。請注意,我們還阻止資料庫類靜态初始化,通過定義特殊的模拟方法”clinit$()”。這是必要的,在這種情況下,因為資料庫類實際上建立了一個會在其靜态初始化執行個體。

每個測試設定特定的模拟電子郵件類通過建立内聯(匿名)模型類執行個體。見這些模拟方法,@Mock注釋可以選擇性地指定确切/最小/最大限制預期/允許調用相應的實際方法。盡管這裡沒有顯示,構造函數可以mock與模拟方法命名為“init$”和有相同的參數構造函數被mock了。