物種起源
任何框架和lib都有其實際需求來源,純粹爲了技術或概念而code的項目,大概都還在象牙塔裏。
Jmock 的首頁上第一句話相當簡明:
JMock is a library that supports test-driven development of Java code with mock objects.
瞭解JMOCK,從兩個概念入手:
TDD
Unit tests are so named because they each test one unit of
code. Whether a module of code has hundreds of unit tests or only five
is irrelevant. A test suite for use in TDD should never cross process
boundaries in a program, let alone network connections. Doing so
introduces delays that make tests run slowly and discourage developers
from running the whole suite. Introducing dependencies on external
modules or data also turns unit tests into integration tests.
If one module misbehaves in a chain of interrelated modules, it is not
so immediately clear where to look for the cause of the failure.
爲什麼叫unit test(單元測試)?單元測試就是測試單個method的功能,這也就是爲什麼我們寫junit測試時,總是一個個testMethodXXX。TDD裏提倡單元測試的單元性,需要將外部依賴性(比如DB, network)隔離出來。有兩個好處,一是讓test 飛一飛,跑的更快,二是界定boundaries,因爲邏輯交叉,一個邏輯失敗,會導致好多測試失敗。
老實說,這兩點有些似是而非。test越接近產品環境,越有可能發現產品環境下的bug。另外continuous integration中說測試可以放在晚上跑,所以速度慢不是問題。連鎖反應是個問題,可能會導致找bug時道路曲折。但測試失敗應該不是個經常發生的問題把。
anyway,怎麼保證隔離呢?that's why need mock..
In a unit test, mock objects can simulate the behavior of complex, real (non-mock) objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test
mock object就是使用method stub來實現定義的interface,比如:
BEGIN ThermometerRead(Source insideOrOutside) RETURN 28 END ThermometerRead
方法不作任何邏輯處理,直接return一個預定的結果。
The Design principle
針對上述概念,最簡單直接的一種實現方法是每個接口增加一個test實現。在需要的地方使用test實現來作測試。這種方法理論上可行, why?
首先你會形成class disaster,增加無數個test class。其次缺少靈活性,在stub實現往往是輸出一個固定結果,而測試時有可能需要針對不同條件來返回不同值。
關於class disaster的問題, jmock使用代理來避免。關於return的問題,jmock引入了action的概念。
簡單點,實現原則:
- proxy
- 聲明方法的調用
- 聲明方法的輸入/輸出
其具體實現,本人到沒有dig。只是對其中的一種語法比較感興趣, Expectations使用雙大括號聲明{{ }} (Double-Brace Block)。java中有這種語法?本文結尾給出揭曉
The usage
hello, jmock
首先引入jmock的lib,使用eclipse中的maven-plugin自動添加
- jmock-2.5.1.jar
- hamcrest-core-1.1.jar
- hamcrest-library-1.1.jar
可能會有類加載的問題,因爲junit 裏面也打包了hamcrest。如果有java.lang.SecurityException class "org.hamcrest.TypeSafeMatcher" 問題。可以將junit改爲junit-no-dep,或者改變類加載的搜索順尋。
要測試的方法爲Publisher的publish方法
我們可以使用一個實現好的Subscriber,在publish之後,通過Subscriber的行爲來判斷msg是否receive。顯然,如引言中所述,引入Subscriber的依賴,單元測試的邊界變得模糊,我們到底是測試publish呢,還是測試receive呢?
jmock 測試code如下:
1. 生成要測試的object,Publisher爲真publisher, Subscriber則是mock生成。
2. 讓target對象使用mock對象,或者將mock對象設置到測試的target對象中
3. 聲明mock對象的expectations,就是mock的哪些方法被調用,調用的輸入輸出是什麼。例子中,聲明subscriber的receive被調用一次,輸入參數是message,沒有輸出。
4. 運行要測試的方法
5. 進行verify, context.assertIsSatisfied(),來assert調用是否和expectation一致。在實際測試中,我們往往加上自己的assert,assert測試方法的return(本例中target方法也沒有輸出)
從上面可以看出,使用jmock的一個關鍵部分就是聲明expectations。
Expectations
expectations的語法如下:
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外,其他都是optional. 其核心無非就是declare方法的調用,其他都浮雲。
(mock-object).method(argument-constraints) will(action);
基本對應以下java語法
Object xx = mock.method(parameter);
return xxx;
具體每個部分說明如下:
Invocation count
方法觸發的次數,使用較多的是
oneOf 觸發一次
exactly(n).of 觸發n次
allowing 任意次
Argument Matchers
方法參數的匹配,使用with聲明,主要使用same, equal, any等
oneOf (mock).doSomething(with(equal(1))); //參數爲單一參數,值equal1
oneOf (mock).doSomething(with(same(1))) // 參數爲單一參數,直 == 1
oneOf (mock).doSomething(with(any(Class<T> type))) //參數爲單一參數,只要是class類型即可。
Actions
method invocation後的,使用will聲明,主要使用return和throwException等
will(returnValue(v))
will(throwException(e))
States
狀態機都出來了。哥還真用了一把。在最先的版本中,我們使用Spring來注入Mockery,所有test共用了一個Mockery,這樣各個unit test的Expecting相互影響。怎麼隔離開來呢?加上state condition
- 在每個測試的開頭聲明一個States
final States pen = context.states("mymethod").startsAs("start");
- 在expecting的method invocation加入when
oneOf (turtle).forward(10); when(pen.is("start"));
- 在測試結束時,將狀態機的狀態設置爲非start的其他狀態
Sequences
invocation 的順序,語法類似於States
Invocations that are expected in a sequence must occur in the order in which they appear in the test code. A test can create more than one sequence and an expectation can be part of more than once sequence at a time.
Mocking class instead of interface
Add jmock-legacy-2.5.1.jar, cglib-nodep-2.1_3.jar and objenesis-1.0.jar to your CLASSPATH. 連final class夜可以被mock,引入jdave即可
Annotation
自動生成mock對象
自動生成States/Sequence
@Auto States pen; @Auto Sequence events;
The limits
- closely couple: unit test 和方法的實現緊耦合。這樣如果你的方法或者類refactor,unit test要做相應的修改
- 當mock object數量很多,維護的成本相應增加
- mock的行爲如果不正確,有可能導致unit test變成non-sense,檢測不出bug
由於我們系統的domain model很複雜(遺留原因),這時引入jmock,我們發現生成mock對象的過程,似乎是將所有Domain object和implementation又重新寫了一遍。所有上述limit一下子暴露無遺。
目前暫時沒有想到有效的應對下述系統的測試方法:
1. db design is complex/legacy
2. domain model (wsdl/xsd) design is complex. because we reuse the old business-rules, while those business-rules is not examine and verify carefully in the past.
一個遺留的db加上一個複雜的domain model,雖然business logic很簡單,沒有任何複雜的商業計算,只是簡單的DAO -> WSDL Bean的操作,實現起來讓開發人員覺得紛繁蕪雜,難以入手。
在本文完成之時,我們決定仍然使用Jmock, 效果如何,下回分解。
關於DoubleBrace
這是一個trick,DoubleBraceInitialization
The first brace creates a new AnonymousInnerClass, the second declares an instance initializer block that is run when the anonymous inner class is instantiated
第一個大括號聲明瞭一個Annoymous的InnerClass,例子中聲明瞭一個Hashset的子類;第二個括號則是 instance initializer block,實例的初始化模塊。expectations的聲明相當明瞭。
修訂
使用mock,除了前文所說的isolate(隔離)外,模擬(simulate)也是另外一個重要原因,比如瀏覽器表單提交。。。
更多
參見cookbook
http://www.jmock.org/cookbook.html
關於更多的java Idioms
http://www.c2.com/cgi/wiki?JavaIdioms