現在的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的。
- public class UserManager {
- public AddressService addressService;
- public Address findAddress(String userName) {
- return addressService.findAddress(userName);
- }
- public Iterator<Address> findAddresses(String userName) {
- return addressService.findAddresses(userName);
- }
- }
我們有一個UserManager,要測試它的方法,但是,UserManager是依賴於AddressService的。這裏我們準備mock掉AddressService。
第1章 jmock初體驗
這個例子的作用在於像一個傳統的hello world一樣,給大家一個簡明的介紹,可以有一個感覺,jmock可以做什麼。
AddressService本身太複雜,很難構建,這個時候,jmock出場了。
- @Test
- public void testFindAddress() {
- // 建立一個test上下文對象。
- Mockery context = new Mockery();
- // 生成一個mock對象
- final AddressService addressServcie = context
- .mock(AddressService.class);
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法被調用一次,並且返回西安。
- oneOf(addressServcie).findAddress("allen");
- will(returnValue(Para.Xian));
- }
- });
- UserManager manager = new UserManager();
- // 設置mock對象
- manager.addressService = addressServcie;
- // 調用方法
- Address result = manager.findAddress("allen");
- // 驗證結果
- Assert.assertEquals(Result.Xian, result);
- }
那麼這裏做了什麼事情呢?
1 首先,我們建立一個test上下文對象。
2 用這個mockery context建立了一個mock對象來mock AddressService.
3 設置了這個mock AddressService的findAddress應該被調用1次,並且參數爲"allen"。
4 生成UserManager對象,設置addressService,調用findAddress。
5 驗證期望被滿足。
基本上,一個簡單的jmock應用大致就是這樣一個流程。
最顯著的優點就是,我們沒有AddressService的具體實現,一樣可以測試對AddressService接口有依賴的其他類的行爲。也就是說,我們通過mock一個對象來隔離這個對象對要測試的代碼的影響。
由於大致的流程是一樣的,我們提供一個抽象類來模板化jmock的使用。
- public abstract class TestBase {
- // 建立一個test上下文對象。
- protected Mockery context = new Mockery();
- // 生成一個mock對象
- protected final AddressService addressServcie = context
- .mock(AddressService.class);
- /**
- * 要測試的userManager.
- * */
- protected UserManager manager;
- /**
- * 設置UserManager,並且設置mock的addressService。
- * */
- private void setUpUserManagerWithMockAddressService() {
- manager = new UserManager();
- // 設置mock對象
- manager.addressService = addressServcie;
- }
- /**
- * 調用findAddress,並且驗證返回值。
- *
- * @param userName
- * userName
- * @param expected
- * 期望返回的地址。
- * */
- protected void assertFindAddress(String userName, Address expected) {
- Address address = manager.findAddress(userName);
- Assert.assertEquals(expected, address);
- }
- /**
- * 調用findAddress,並且驗證方法拋出異常。
- * */
- protected void assertFindAddressFail(String userName) {
- try {
- manager.findAddress(userName);
- Assert.fail();
- } catch (Throwable t) {
- // Nothing to do.
- }
- }
- @Test
- public final void test() {
- setUpExpectatioin();
- setUpUserManagerWithMockAddressService();
- invokeAndVerify();
- }
- /**
- * 建立期望。
- * */
- protected abstract void setUpExpectatioin();
- /**
- * 調用方法並且驗證結果。
- * */
- protected abstract void invokeAndVerify();
- }
這樣一來,我們以後的例子中只用關心setUpExpectatioin()和invokeAndVerify()方法就好了。
第2章 期望
好了,讓我們來看看一個期望的框架。
- invocation-count (mock-object).method(argument-constraints);
- inSequence(sequence-name);
- when(state-machine.is(state-name));
- will(action);
- 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)。
- @Override
- protected void setUpExpectatioin() {
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress("allen");
- will(returnValue(Para.BeiJing));
- // 當參數爲null的時候,拋出IllegalArgumentException異常。
- allowing(addressServcie).findAddress(null);
- will(throwException(new IllegalArgumentException()));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.BeiJing);
- assertFindAddressFail(null);
- }
這裏演示了兩種調用方法的結果,返回值和拋異常。
使用jmock可以返回常量值,也可以根據變量生成返回值。
拋異常是同樣的,可以模擬在不同場景下拋的各種異常。
對於Iterator的返回值,jmock也提供了特殊支持。
- @Override
- protected void setUpExpectatioin() {
- // 生成地址列表
- final List<Address> addresses = new ArrayList<Address>();
- addresses.add(Para.Xian);
- addresses.add(Para.HangZhou);
- final Iterator<Address> iterator = addresses.iterator();
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddresses方法用returnvalue返回一個Iterator<Address>對象。
- allowing(addressServcie).findAddresses("allen");
- will(returnValue(iterator));
- // 當參數爲"dandan"的時候,addressServcie對象的findAddresses方法用returnIterator返回一個Iterator<Address>對象。
- allowing(addressServcie).findAddresses("dandan");
- will(returnIterator(addresses));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- Iterator<Address> resultIterator = null;
- // 第1次以"allen"調用方法
- resultIterator = manager.findAddresses("allen");
- // 斷言返回的對象。
- assertIterator(resultIterator);
- // 第2次以"allen"調用方法,返回的與第一次一樣的iterator結果對象,所以這裏沒有next了。
- resultIterator = manager.findAddresses("allen");
- Assert.assertFalse(resultIterator.hasNext());
- // 第1次以"dandan"調用方法
- resultIterator = manager.findAddresses("dandan");
- // 斷言返回的對象。
- assertIterator(resultIterator);
- // 第2次以"dandan"調用方法,返回的是一個全新的iterator。
- resultIterator = manager.findAddresses("dandan");
- // 斷言返回的對象。
- assertIterator(resultIterator);
- }
- /** 斷言resultIterator中有兩個期望的Address */
- private void assertIterator(Iterator<Address> resultIterator) {
- Address address = null;
- // 斷言返回的對象。
- address = resultIterator.next();
- Assert.assertEquals(Result.Xian, address);
- address = resultIterator.next();
- Assert.assertEquals(Result.HangZhou, address);
- // 沒有Address了。
- Assert.assertFalse(resultIterator.hasNext());
- }
從這個例子可以看到對於Iterator,returnValue和returnIterator的不同。
- @Override
- protected void setUpExpectatioin() {
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress("allen");
- will(new Action() {
- @Override
- public Object invoke(Invocation invocation)
- throws Throwable {
- return Para.Xian;
- }
- @Override
- public void describeTo(Description description) {
- }
- });
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.Xian);
- }
其實這裏要返回一個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的使用。
- @Override
- protected void setUpExpectatioin() {
- // 設置期望。
- context.checking(new Expectations() {
- {
- // doAllAction
- allowing(addressServcie).findAddress("allen");
- will(doAll(returnValue(Para.Xian), returnValue(Para.HangZhou)));
- // ActionSequence
- allowing(addressServcie).findAddress("dandan");
- will(onConsecutiveCalls(returnValue(Para.Xian),
- returnValue(Para.HangZhou)));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.HangZhou);
- assertFindAddress("dandan", Result.Xian);
- assertFindAddress("dandan", Result.HangZhou);
- }
第4章 參數匹配
即設置argument-constraints
- @Override
- protected void setUpExpectatioin() {
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress("allen");
- will(returnValue(Para.Xian));
- // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress(with(equal("dandan")));
- will(returnValue(Para.HangZhou));
- // 當參數包含"zhi"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress(
- with(new BaseMatcher<String>() {
- @Override
- public boolean matches(Object item) {
- String value = (String) item;
- if (value == null)
- return false;
- return value.contains("zhi");
- }
- @Override
- public void describeTo(Description description) {
- }
- }));
- will(returnValue(Para.BeiJing));
- // 當參數爲其他任何值的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- allowing(addressServcie).findAddress(with(any(String.class)));
- will(returnValue(Para.ShangHai));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- // 以"allen"調用方法
- assertFindAddress("allen", Result.Xian);
- // 以"dandan"調用方法
- assertFindAddress("dandan", Result.HangZhou);
- // 以包含"zhi"的參數調用方法
- assertFindAddress("abczhidef", Result.BeiJing);
- // 以任意一個字符串"abcdefg"調用方法
- assertFindAddress("abcdefg", Result.ShangHai);
- }
測試演示了直接匹配,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章 指定執行序列
- @Override
- protected void setUpExpectatioin() {
- final Sequence sequence = context.sequence("mySeq_01");
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- oneOf(addressServcie).findAddress("allen");
- inSequence(sequence);
- will(returnValue(Para.Xian));
- // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- oneOf(addressServcie).findAddress("dandan");
- inSequence(sequence);
- will(returnValue(Para.HangZhou));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.Xian);
- assertFindAddress("dandan", Result.HangZhou);
- }
這裏指定了調用的序列。使得調用必須以指定的順序調用。
來看一個反例
- @Override
- protected void setUpExpectatioin() {
- final Sequence sequence = context.sequence("mySeq_01");
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 當參數爲"allen"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- oneOf(addressServcie).findAddress("allen");
- inSequence(sequence);
- will(returnValue(Para.Xian));
- // 當參數爲"dandan"的時候,addressServcie對象的findAddress方法返回一個Adress對象。
- oneOf(addressServcie).findAddress("dandan");
- inSequence(sequence);
- will(returnValue(Para.HangZhou));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddressFail("dandan");
- }
當指定序列的第一個調用沒有觸發的時候,直接調用第2個,則會拋異常。
Note:指定序列的時候注意方法調用次數這個約束,如果是allowing那麼在這個序列中,它是可以被忽略的。
第7章 狀態機
狀態機的作用在於模擬對象在什麼狀態下調用才用觸發。
- @Override
- protected void setUpExpectatioin() {
- final States states = context.states("sm").startsAs("s1");
- // 設置期望。
- context.checking(new Expectations() {
- {
- // 狀態爲s1參數包含allen的時候返回西安
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("allen")));
- when(states.is("s1"));
- will(returnValue(Para.Xian));
- // 狀態爲s1參數包含dandan的時候返回杭州,跳轉到s2。
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("dandan")));
- when(states.is("s1"));
- will(returnValue(Para.HangZhou));
- then(states.is("s2"));
- // 狀態爲s2參數包含allen的時候返回上海
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("allen")));
- when(states.is("s2"));
- will(returnValue(Para.ShangHai));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- // s1狀態
- assertFindAddress("allen", Result.Xian);
- assertFindAddress("allen0", Result.Xian);
- // 狀態跳轉到 s2
- assertFindAddress("dandan", Result.HangZhou);
- // s2狀態
- assertFindAddress("allen", Result.ShangHai);
- }
可以看到,如果序列一樣,狀態也爲期望的執行設置了約束,這裏就是用狀態來約束哪個期望應該被執行。
可以用is或者isNot來限制狀態。
狀態機有一個很好的用處。
當我們建立一個test執行上下文的時候,如果建立的時候和執行的時候,我們都需要調用mock ojbect的方法,那麼我們可以用狀態機把這兩部分隔離開。讓他們在不同的狀態下執行。