概述
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了。