jmock2.5基本教程

第0章 概述

現在的dev不是僅僅要寫code而已,UT已經變爲開發中不可缺少的一環。JUnit的出現給javaer的UT編寫提供了巨大的便利。但是JUnit並沒有解決所有的問題。
當我們要測試一個功能點的時候,需要把不需要我們關注的東西隔離開,從而可以只關注我們需要關注的行爲。
jmock通過mock對象來模擬一個對象的行爲,從而隔離開我們不關心的其他對象,使得UT的編寫變得更爲可行,也使得TDD變得更爲方便,自然而然的,也就成爲敏捷開發的一個利器。

可以到http://www.jmock.org/download.html下載jmock.
添加jar到classpath。
添加的時候,注意把JUnit4的order放到最後。因爲junit4它自己帶了一個Hamcrest jar。
要是不注意順序的話,有可能報
java.lang.SecurityException: class "org.hamcrest.TypeSafeMatcher"'s signer information does not match signer information of other classes in the same package。

Note:
這裏的類定義用來演示如何使用jmock,所以都是定義爲public的。

Java代碼  收藏代碼
  1. public class UserManager {  
  2.   
  3.     public AddressService addressService;  
  4.   
  5.     public Address findAddress(String userName) {  
  6.         return addressService.findAddress(userName);  
  7.     }  
  8.   
  9.     public Iterator<Address> findAddresses(String userName) {  
  10.         return addressService.findAddresses(userName);  
  11.     }  
  12. }  

我們有一個UserManager,要測試它的方法,但是,UserManager是依賴於AddressService的。這裏我們準備mock掉AddressService。


第1章 jmock初體驗

這個例子的作用在於像一個傳統的hello world一樣,給大家一個簡明的介紹,可以有一個感覺,jmock可以做什麼。
AddressService本身太複雜,很難構建,這個時候,jmock出場了。
Java代碼  收藏代碼
  1. @Test  
  2. public void testFindAddress() {  
  3.   
  4.     // 建立一個test上下文對象。  
  5.     Mockery context = new Mockery();  
  6.   
  7.     // 生成一個mock對象  
  8.     final AddressService addressServcie = context  
  9.             .mock(AddressService.class);  
  10.   
  11.     // 設置期望。  
  12.     context.checking(new Expectations() {  
  13.         {  
  14.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法被調用一次,並且返回西安。  
  15.             oneOf(addressServcie).findAddress("allen");  
  16.             will(returnValue(Para.Xian));  
  17.         }  
  18.     });  
  19.   
  20.     UserManager manager = new UserManager();  
  21.   
  22.     // 設置mock對象  
  23.     manager.addressService = addressServcie;  
  24.   
  25.     // 調用方法  
  26.     Address result = manager.findAddress("allen");  
  27.   
  28.     // 驗證結果  
  29.     Assert.assertEquals(Result.Xian, result);  
  30.   
  31. }  

那麼這裏做了什麼事情呢?
1 首先,我們建立一個test上下文對象。
2 用這個mockery context建立了一個mock對象來mock AddressService.
3 設置了這個mock AddressService的findAddress應該被調用1次,並且參數爲"allen"。
4 生成UserManager對象,設置addressService,調用findAddress。
5 驗證期望被滿足。

基本上,一個簡單的jmock應用大致就是這樣一個流程。

最顯著的優點就是,我們沒有AddressService的具體實現,一樣可以測試對AddressService接口有依賴的其他類的行爲。也就是說,我們通過mock一個對象來隔離這個對象對要測試的代碼的影響。

由於大致的流程是一樣的,我們提供一個抽象類來模板化jmock的使用。
Java代碼  收藏代碼
  1. public abstract class TestBase {  
  2.   
  3.     // 建立一個test上下文對象。  
  4.     protected Mockery context = new Mockery();  
  5.   
  6.     // 生成一個mock對象  
  7.     protected final AddressService addressServcie = context  
  8.             .mock(AddressService.class);  
  9.   
  10.     /** 
  11.      * 要測試的userManager. 
  12.      * */  
  13.     protected UserManager manager;  
  14.   
  15.     /** 
  16.      * 設置UserManager,並且設置mock的addressService。 
  17.      * */  
  18.     private void setUpUserManagerWithMockAddressService() {  
  19.         manager = new UserManager();  
  20.         // 設置mock對象  
  21.         manager.addressService = addressServcie;  
  22.     }  
  23.   
  24.     /** 
  25.      * 調用findAddress,並且驗證返回值。 
  26.      *  
  27.      * @param userName 
  28.      *            userName 
  29.      * @param expected 
  30.      *            期望返回的地址。 
  31.      * */  
  32.     protected void assertFindAddress(String userName, Address expected) {  
  33.         Address address = manager.findAddress(userName);  
  34.         Assert.assertEquals(expected, address);  
  35.     }  
  36.   
  37.     /** 
  38.      * 調用findAddress,並且驗證方法拋出異常。 
  39.      * */  
  40.     protected void assertFindAddressFail(String userName) {  
  41.         try {  
  42.             manager.findAddress(userName);  
  43.             Assert.fail();  
  44.         } catch (Throwable t) {  
  45.             // Nothing to do.  
  46.         }  
  47.     }  
  48.   
  49.     @Test  
  50.     public final void test() {  
  51.   
  52.         setUpExpectatioin();  
  53.   
  54.         setUpUserManagerWithMockAddressService();  
  55.   
  56.         invokeAndVerify();  
  57.     }  
  58.   
  59.     /** 
  60.      * 建立期望。 
  61.      * */  
  62.     protected abstract void setUpExpectatioin();  
  63.   
  64.     /** 
  65.      * 調用方法並且驗證結果。 
  66.      * */  
  67.     protected abstract void invokeAndVerify();  
  68. }  

這樣一來,我們以後的例子中只用關心setUpExpectatioin()和invokeAndVerify()方法就好了。

第2章 期望

好了,讓我們來看看一個期望的框架。
Java代碼  收藏代碼
  1. invocation-count (mock-object).method(argument-constraints);  
  2.     inSequence(sequence-name);  
  3.     when(state-machine.is(state-name));  
  4.     will(action);  
  5.     then(state-machine.is(new-state-name));  


invocation-count 調用的次數約束
mock-object mock對象
method 方法
argument-constraints 參數約束
inSequence 順序
when 當mockery的狀態爲指定的時候觸發。
will(action) 方法觸發的動作
then 方法觸發後設置mockery的狀態

這個稍微複雜一些,一下子不明白是正常的,後面講到其中的細節時,可以回來在看看這個框架。

第3章 返回值

調用一個方法,可以設置它的返回值。即設置will(action)。
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.     context.checking(new Expectations() {  
  4.         {  
  5.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  6.             allowing(addressServcie).findAddress("allen");  
  7.             will(returnValue(Para.BeiJing));  
  8.   
  9.             // 當參數爲null的時候,拋出IllegalArgumentException異常。  
  10.             allowing(addressServcie).findAddress(null);  
  11.             will(throwException(new IllegalArgumentException()));  
  12.         }  
  13.     });  
  14. }  
  15.   
  16. @Override  
  17. protected void invokeAndVerify() {  
  18.     assertFindAddress("allen", Result.BeiJing);  
  19.     assertFindAddressFail(null);  
  20. }  


這裏演示了兩種調用方法的結果,返回值和拋異常。
使用jmock可以返回常量值,也可以根據變量生成返回值。
拋異常是同樣的,可以模擬在不同場景下拋的各種異常。

對於Iterator的返回值,jmock也提供了特殊支持。
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.     // 生成地址列表  
  4.     final List<Address> addresses = new ArrayList<Address>();  
  5.     addresses.add(Para.Xian);  
  6.     addresses.add(Para.HangZhou);  
  7.   
  8.     final Iterator<Address> iterator = addresses.iterator();  
  9.   
  10.     // 設置期望。  
  11.     context.checking(new Expectations() {  
  12.         {  
  13.             // 當參數爲"allen"的時候,addressServcie對象的findAddresses方法用returnvalue返回一個Iterator<Address>對象。  
  14.             allowing(addressServcie).findAddresses("allen");  
  15.             will(returnValue(iterator));  
  16.   
  17.             // 當參數爲"dandan"的時候,addressServcie對象的findAddresses方法用returnIterator返回一個Iterator<Address>對象。  
  18.             allowing(addressServcie).findAddresses("dandan");  
  19.             will(returnIterator(addresses));  
  20.         }  
  21.     });  
  22.   
  23. }  
  24.   
  25. @Override  
  26. protected void invokeAndVerify() {  
  27.   
  28.     Iterator<Address> resultIterator = null;  
  29.   
  30.     // 第1次以"allen"調用方法  
  31.     resultIterator = manager.findAddresses("allen");  
  32.     // 斷言返回的對象。  
  33.     assertIterator(resultIterator);  
  34.   
  35.     // 第2次以"allen"調用方法,返回的與第一次一樣的iterator結果對象,所以這裏沒有next了。  
  36.     resultIterator = manager.findAddresses("allen");  
  37.     Assert.assertFalse(resultIterator.hasNext());  
  38.   
  39.     // 第1次以"dandan"調用方法  
  40.     resultIterator = manager.findAddresses("dandan");  
  41.     // 斷言返回的對象。  
  42.     assertIterator(resultIterator);  
  43.   
  44.     // 第2次以"dandan"調用方法,返回的是一個全新的iterator。  
  45.     resultIterator = manager.findAddresses("dandan");  
  46.     // 斷言返回的對象。  
  47.     assertIterator(resultIterator);  
  48. }  
  49.   
  50. /** 斷言resultIterator中有兩個期望的Address */  
  51. private void assertIterator(Iterator<Address> resultIterator) {  
  52.     Address address = null;  
  53.     // 斷言返回的對象。  
  54.     address = resultIterator.next();  
  55.     Assert.assertEquals(Result.Xian, address);  
  56.     address = resultIterator.next();  
  57.     Assert.assertEquals(Result.HangZhou, address);  
  58.     // 沒有Address了。  
  59.     Assert.assertFalse(resultIterator.hasNext());  
  60. }  

從這個例子可以看到對於Iterator,returnValue和returnIterator的不同。

Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.     // 設置期望。  
  4.     context.checking(new Expectations() {  
  5.         {  
  6.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  7.             allowing(addressServcie).findAddress("allen");  
  8.             will(new Action() {  
  9.   
  10.                 @Override  
  11.                 public Object invoke(Invocation invocation)  
  12.                         throws Throwable {  
  13.                     return Para.Xian;  
  14.                 }  
  15.   
  16.                 @Override  
  17.                 public void describeTo(Description description) {  
  18.                 }  
  19.             });  
  20.         }  
  21.     });  
  22. }  
  23.   
  24. @Override  
  25. protected void invokeAndVerify() {  
  26.     assertFindAddress("allen", Result.Xian);  
  27. }  


其實這裏要返回一個Action,該Action負責返回調用的返回值。既然知道了這個道理,我們自然可以自定義Action來返回方法調用的結果。
而returnValue,returnIterator,throwException只不過是一些Expectations提供的一些static方法用來方便的構建不同的Action。

除了剛纔介紹的
ReturnValueAction 直接返回結果
ThrowAction 拋出異常
ReturnIteratorAction 返回Iterator
還有
VoidAction 
ReturnEnumerationAction 返回Enumeration
DoAllAction 所有的Action都執行,但是隻返回最後一個Action的結果。
ActionSequence 每次調用返回其Actions列表中的下一個Action的結果。
CustomAction 一個抽象的Action,方便自定義Action。

舉個例子來說明DoAllAction和ActionSequence的使用。
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.     // 設置期望。  
  4.     context.checking(new Expectations() {  
  5.         {  
  6.             // doAllAction  
  7.             allowing(addressServcie).findAddress("allen");  
  8.             will(doAll(returnValue(Para.Xian), returnValue(Para.HangZhou)));  
  9.   
  10.             // ActionSequence  
  11.             allowing(addressServcie).findAddress("dandan");  
  12.             will(onConsecutiveCalls(returnValue(Para.Xian),  
  13.                     returnValue(Para.HangZhou)));  
  14.         }  
  15.     });  
  16. }  
  17.   
  18. @Override  
  19. protected void invokeAndVerify() {  
  20.     assertFindAddress("allen", Result.HangZhou);  
  21.   
  22.     assertFindAddress("dandan", Result.Xian);  
  23.     assertFindAddress("dandan", Result.HangZhou);  
  24.   
  25. }  



第4章 參數匹配

即設置argument-constraints
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.     // 設置期望。  
  4.     context.checking(new Expectations() {  
  5.         {  
  6.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  7.             allowing(addressServcie).findAddress("allen");  
  8.             will(returnValue(Para.Xian));  
  9.   
  10.             // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  11.             allowing(addressServcie).findAddress(with(equal("dandan")));  
  12.             will(returnValue(Para.HangZhou));  
  13.   
  14.             // 當參數包含"zhi"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  15.             allowing(addressServcie).findAddress(  
  16.                     with(new BaseMatcher<String>() {  
  17.   
  18.                         @Override  
  19.                         public boolean matches(Object item) {  
  20.                             String value = (String) item;  
  21.                             if (value == null)  
  22.                                 return false;  
  23.                             return value.contains("zhi");  
  24.                         }  
  25.   
  26.                         @Override  
  27.                         public void describeTo(Description description) {  
  28.                         }  
  29.   
  30.                     }));  
  31.   
  32.             will(returnValue(Para.BeiJing));  
  33.   
  34.             // 當參數爲其他任何值的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  35.             allowing(addressServcie).findAddress(with(any(String.class)));  
  36.   
  37.             will(returnValue(Para.ShangHai));  
  38.         }  
  39.     });  
  40.   
  41. }  
  42.   
  43. @Override  
  44. protected void invokeAndVerify() {  
  45.     // 以"allen"調用方法  
  46.     assertFindAddress("allen", Result.Xian);  
  47.     // 以"dandan"調用方法  
  48.     assertFindAddress("dandan", Result.HangZhou);  
  49.     // 以包含"zhi"的參數調用方法  
  50.     assertFindAddress("abczhidef", Result.BeiJing);  
  51.     // 以任意一個字符串"abcdefg"調用方法  
  52.     assertFindAddress("abcdefg", Result.ShangHai);  
  53. }  

測試演示了直接匹配,equal匹配,自定義匹配,任意匹配。
其實,這些都是爲了給參數指定一個Matcher,來決定調用方法的時候,是否接收這個參數。
在Expectations中提供了一些便利的方法方便我們構造Matcher.
其中
equal判斷用equal方法判斷是否相等。
same判斷是否是同一個引用。
any,anything接收任意值。
aNull接收null。
aNonNull接收非null.

jmock提供了很多有用的匹配。可以用來擴展寫出更多的Matcher。
基本Matcher
IsSame 引用相等。
IsNull
IsInstanceOf 
IsEqual 考慮了數組的相等(長度相等,內容equals)
IsAnything always return true.

邏輯Matcher
IsNot
AnyOf
AllOf

其他
Is 裝飾器模式的Matcher,使得可讀性更高。

第5章 指定方法調用次數

可以指定方法調用的次數。即對invocation-count進行指定。
exactly 精確多少次
oneOf 精確1次
atLeast 至少多少次
between 一個範圍
atMost 至多多少次
allowing 任意次
ignoring 忽略
never 從不執行

可以看出,這些range都是很明瞭的。只有allowing和ignoring比較特殊,這兩個的實際效果是一樣的,但是關注點不一樣。當我們允許方法可以任意次調用時,用allowing,當我們不關心一個方法的調用時,用ignoring。

第6章 指定執行序列
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.   
  4.     final Sequence sequence = context.sequence("mySeq_01");  
  5.   
  6.     // 設置期望。  
  7.     context.checking(new Expectations() {  
  8.         {  
  9.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  10.             oneOf(addressServcie).findAddress("allen");  
  11.             inSequence(sequence);  
  12.             will(returnValue(Para.Xian));  
  13.   
  14.             // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  15.             oneOf(addressServcie).findAddress("dandan");  
  16.             inSequence(sequence);  
  17.             will(returnValue(Para.HangZhou));  
  18.   
  19.         }  
  20.     });  
  21.   
  22. }  
  23.   
  24. @Override  
  25. protected void invokeAndVerify() {  
  26.     assertFindAddress("allen", Result.Xian);  
  27.     assertFindAddress("dandan", Result.HangZhou);  
  28. }  

這裏指定了調用的序列。使得調用必須以指定的順序調用。
來看一個反例
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.   
  4.     final Sequence sequence = context.sequence("mySeq_01");  
  5.   
  6.     // 設置期望。  
  7.     context.checking(new Expectations() {  
  8.         {  
  9.             // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  10.             oneOf(addressServcie).findAddress("allen");  
  11.             inSequence(sequence);  
  12.             will(returnValue(Para.Xian));  
  13.   
  14.             // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。  
  15.             oneOf(addressServcie).findAddress("dandan");  
  16.             inSequence(sequence);  
  17.             will(returnValue(Para.HangZhou));  
  18.   
  19.         }  
  20.     });  
  21. }  
  22.   
  23. @Override  
  24. protected void invokeAndVerify() {  
  25.     assertFindAddressFail("dandan");  
  26. }  

當指定序列的第一個調用沒有觸發的時候,直接調用第2個,則會拋異常。
Note:指定序列的時候注意方法調用次數這個約束,如果是allowing那麼在這個序列中,它是可以被忽略的。


第7章 狀態機
狀態機的作用在於模擬對象在什麼狀態下調用才用觸發。
Java代碼  收藏代碼
  1. @Override  
  2. protected void setUpExpectatioin() {  
  3.   
  4.     final States states = context.states("sm").startsAs("s1");  
  5.   
  6.     // 設置期望。  
  7.     context.checking(new Expectations() {  
  8.         {  
  9.             // 狀態爲s1參數包含allen的時候返回西安  
  10.             allowing(addressServcie).findAddress(  
  11.                     with(StringContains.containsString("allen")));  
  12.             when(states.is("s1"));  
  13.             will(returnValue(Para.Xian));  
  14.   
  15.             // 狀態爲s1參數包含dandan的時候返回杭州,跳轉到s2。  
  16.             allowing(addressServcie).findAddress(  
  17.                     with(StringContains.containsString("dandan")));  
  18.             when(states.is("s1"));  
  19.             will(returnValue(Para.HangZhou));  
  20.             then(states.is("s2"));  
  21.   
  22.             // 狀態爲s2參數包含allen的時候返回上海  
  23.             allowing(addressServcie).findAddress(  
  24.                     with(StringContains.containsString("allen")));  
  25.             when(states.is("s2"));  
  26.             will(returnValue(Para.ShangHai));  
  27.         }  
  28.     });  
  29. }  
  30.   
  31. @Override  
  32. protected void invokeAndVerify() {  
  33.     // s1狀態  
  34.     assertFindAddress("allen", Result.Xian);  
  35.     assertFindAddress("allen0", Result.Xian);  
  36.   
  37.     // 狀態跳轉到 s2  
  38.     assertFindAddress("dandan", Result.HangZhou);  
  39.   
  40.     // s2狀態  
  41.     assertFindAddress("allen", Result.ShangHai);  
  42. }  

可以看到,如果序列一樣,狀態也爲期望的執行設置了約束,這裏就是用狀態來約束哪個期望應該被執行。
可以用is或者isNot來限制狀態。

狀態機有一個很好的用處。
當我們建立一個test執行上下文的時候,如果建立的時候和執行的時候,我們都需要調用mock ojbect的方法,那麼我們可以用狀態機把這兩部分隔離開。讓他們在不同的狀態下執行。
發佈了90 篇原創文章 · 獲贊 9 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章