介紹
本文將介紹模擬測試框架Mockito的一些基礎概念, 介紹該框架的優點,講解應用Mockito的Java示例。
模擬(Mock)的概念?
在軟件開發的世界之外, "mock"一詞是指模仿或者效仿。因此可以將“mock”理解為一個替身,替代者。在軟件開發中提及"mock",通常理解為模擬對象或者fake。
譯者注:mock等多代表的是對被模擬對象的抽象類,你可以把fake理解為mock的實例。不知道這樣說準不準確:)
Fake通常被用作被測類的依賴關系的替代者.。
| 名詞定義?? |
| 依賴關系?– 依賴關系是指在應用程序中一個類基于另一個類來執行其預定的功能。依賴關系通常都存在于所依賴的類的實例變量中。 |
| ? |
| 被測類?– 在編寫單元測試的時候,“單元”一詞通常代表一個單獨的類及為其編寫的測試代碼。被測類指的就是其中被測試的類。 |
為什么需要模擬??
在我們一開始學編程時,我們所寫的對象通常都是獨立的。hello world之類的類并不依賴其他的類(System.out除外),也不會操作別的類。但實際上軟件中是充滿依賴關系的。我們會基于service類寫操作類,而service類又是基于數據訪問類(DAOs)的,依次下去。
圖1 類的依賴關系
單元測試的思路就是我們想在不涉及依賴關系的情況下測試代碼。這種測試可以讓你無視代碼的依賴關系去測試代碼的有效性。核心思想就是如果代碼按設計正常工作,并且依賴關系也正常,那么他們應該會同時工作正常。
下面的代碼就是這樣的例子:
[java]?view plaincopy
import?java.util.ArrayList;?? public?class?Counter?{?? ?????public?Counter()?{?? ?????}?? ?????public?int?count(ArrayList?items)?{?? ??????????int?results?=?0;?? ??????????for(Object?curItem?:?items)?{?? ???????????????results?++;?? ??????????}?? ??????????return?results;?? ?????}?? }??? 如你所見,上面的例子十分簡單,但它闡明了要點。當你想要測試count方法時,你會針對count方法本身如何工作去寫測試代碼。你不會去測試ArrayList是否正常工作,因為你默認它已經被測過并且工作正常。你唯一的目標就是測試對ArrayList的使用。
模擬對象的概念就是我們想要創建一個可以替代實際對象的對象。這個模擬對象要可以通過特定參數調用特定的方法,并且能返回預期結果。
模擬有哪些關鍵點?
在談到模擬時,你只需關心三樣東西:設置測試數據,設定預期結果,驗證結果。一些單元測試方案根本就不涉及這些,有的只涉及設置測試數據,有的只涉及設定預期結果和驗證。
Stubbing?(樁)
Stubbing就是告訴fake當與之交互時執行何種行為過程。通常它可以用來提供那些測試所需的公共屬性(像getters和setters)和公共方法。
當談到stubbing方法,通常你有一系列的選擇?;蛟S你希望返回一個特殊的值,拋出一個錯誤或者觸發一個事件,此外,你可能希望指出方法被調用時的不同行為(即通過傳遞匹配的類型或者參數給方法)。
這咋一聽起來工作量很大,但通常并非這樣。許多mocking框架的一個重要功能就是你不需要提供stub 的實體方法,也不用在執行測試期間stub那些未被調用的方法或者未使用的屬性。
設置預期
Fake的一個關鍵的特性就是當你用它進行模擬測試時你能夠告訴它你預期的結果。例如,你可以要求一個特定的函數被準確的調用3次,或不被調用,或調用至少兩次但不超過5次,或者需要滿足特定類型的參數、特定值和以上任意的組合的調用??赡苄允菬o窮的。
通過設定預期結果告訴fake你期望發生的事情。因為它是一個模擬測試,所以實際上什么也沒發生。但是,對于被測試的類來說,它并無法區分這種情況。所以fake能夠調用函數并讓它做它該做的。
值得注意的是,大多數模擬框架除了可以創建接口的模擬測試外,還可以創建公有類的模擬測試。
驗證預期結果
設置預期和驗證預期是同時進行的。設置預期在調用測試類的函數之前完成,驗證預期則在它之后。所以,首先你設定好預期結果,然后去驗證你的預期結果是否正確。
在一個單元測試中,如果你設定的預期沒有得到滿足,那么這個單元測試就是失敗了。例如,你設置預期結果是?ILoginService.login函數必須用特定的用戶名和密碼被調用一次,但是在測試中它并沒有被調用,這個fake沒被驗證,所以測試失敗。
模擬的好處是什么??
???提前創建測試; TDD(測試驅動開發)??
這是個最大的好處吧。如果你創建了一個Mock那么你就可以在service接口創建之前寫Service Tests了,這樣你就能在開發過程中把測試添加到你的自動化測試環境中了。換句話說,模擬使你能夠使用測試驅動開發。?
???團隊可以并行工作?
這類似于上面的那點;為不存在的代碼創建測試。但前面講的是開發人員編寫測試程序,這里說的是測試團隊來創建。當還沒有任何東西要測的時候測試團隊如何來創建測試呢?模擬并針對模擬測試!這意味著當service借口需要測試時,實際上QA團隊已經有了一套完整的測試組件;沒有出現一個團隊等待另一個團隊完成的情況。這使得模擬的效益型尤為突出了。?
???你可以創建一個驗證或者演示程序。?
由于Mocks非常高效,Mocks可以用來創建一個概念證明,作為一個示意圖,或者作為一個你正考慮構建項目的演示程序。這為你決定項目接下來是否要進行提供了有力的基礎,但最重要的還是提供了實際的設計決策。?
??為無法訪問的資源編寫測試?
這個好處不屬于實際效益的一種,而是作為一個必要時的“救生圈”。有沒有遇到這樣的情況?當你想要測試一個service接口,但service需要經過防火墻訪問,防火墻不能為你打開或者你需要認證才能訪問。遇到這樣情況時,你可以在你能訪問的地方使用MockService替代,這就是一個“救生圈”功能。?
??Mock?可以分發給用戶?
????? 在有些情況下,某種原因你需要允許一些外部來源訪問你的測試系統,像合作伙伴或者客戶。這些原因導致別人也可以訪問你的敏感信息,而你或許只是想允許訪問部分測試環境。在這種情況下,如何向合作伙伴或者客戶提供一個測試系統來開發或者做測試呢?最簡單的就是提供一個mock,無論是來自于你的網絡或者客戶的網絡。soapUI mock非常容易配置,他可以運行在soapUI或者作為一個war包發布到你的java服務器里面。?
??隔離系統?
????? 有時,你希望在沒有系統其他部分的影響下測試系統單獨的一部分。由于其他系統部分會給測試數據造成干擾,影響根據數據收集得到的測試結論。使用mock你可以移除掉除了需要測試部分的系統依賴的模擬。當隔離這些mocks后,mocks就變得非常簡單可靠,快速可預見。這為你提供了一個移除了隨機行為,有重復模式并且可以監控特殊系統的測試環境。?
Mockito 框架
Mockito 是一個基于MIT協議的開源java測試框架。?
Mockito區別于其他模擬框架的地方主要是允許開發者在沒有建立“預期”時驗證被測系統的行為。對mock對象的一個批評是測試代碼與被測系統高度耦合,由于Mockito試圖通過移除“期望規范”來去除expect-run-verify模式(期望--運行--驗證模式),因此使耦合度降低到最低。這樣的突出特性簡化了測試代碼,使它更容易閱讀和修改了。
你可以驗證交互:
[java]?view plaincopy
?? List?mockedList?=?mock(List.class);?? ?? mockedList.add("one");?? mockedList.clear();?? ?? verify(mockedList).add("one");?? verify(mockedList).clear();???? 或者存根方法調用:
[java]?view plaincopy
?? LinkedList?mockedList?=?mock(LinkedList.class);?? ?? when(mockedList.get(0)).thenReturn("first");?? ?? System.out.println(mockedList.get(0));?? ?? System.out.println(mockedList.get(999));??? 一個使用Mockito框架的簡單Java代碼示例
圖2 不使用Mock框架
圖3 使用Mockito框架
步驟 1: ?在IDE中創建一個普通的Java項目?
在Eclipse、NetBeans或IntelliJ IDEA中創建一個普通的Java項目。
步驟 2: ?添加java源碼 ?
類Person.java:
[java]?view plaincopy
package?mockitodemo;?? ?? public?class?Person?? {?? ????private?final?Integer?personID;?? ????private?final?String?personName;?? ????public?Person(?Integer?personID,?String?personName?)?? ????{?? ????????this.personID?=?personID;?? ????????this.personName?=?personName;?? ????}?? ????public?Integer?getPersonID()?? ????{?? ????????return?personID;?? ????}?? ????public?String?getPersonName()?? ????{?? ????????return?personName;?? ????}?? }??? 接口PersonDAO.java:
[java]?view plaincopy
package?mockitodemo;?? ?? public?interface?PersonDao?? {?? ????public?Person?fetchPerson(?Integer?personID?);?? ????public?void?update(?Person?person?);?? }??? 類PersonService.java:
[java]?view plaincopy
package?mockitodemo;?? ?? public?class?PersonService?? {?? ????private?final?PersonDao?personDao;?? ????public?PersonService(?PersonDao?personDao?)?? ????{?? ????????this.personDao?=?personDao;?? ????}?? ????public?boolean?update(?Integer?personId,?String?name?)?? ????{?? ????????Person?person?=?personDao.fetchPerson(?personId?);?? ????????if(?person?!=?null?)?? ????????{?? ????????????Person?updatedPerson?=?new?Person(?person.getPersonID(),?name?);?? ????????????personDao.update(?updatedPerson?);?? ????????????return?true;?? ????????}?? ????????else?? ????????{?? ????????????return?false;?? ????????}?? ????}?? }??? 步驟 3: ?添加單元測試類.??
接下來為類PersonService.java創建單元測試用例。我們使用JUnit 4.x和Mockito 1.9.5。可以設計測試用例類PersionServiceTest.java為如下,代碼中有詳細注釋說明:
[java]?view plaincopy
package?mockitodemo;?? ?? import?org.junit.After;?? import?org.junit.AfterClass;?? import?org.junit.Before;?? import?org.junit.BeforeClass;?? import?org.junit.Test;?? import?static?org.junit.Assert.*;?? import?org.mockito.Mock;?? import?org.mockito.MockitoAnnotations;?? import?org.mockito.ArgumentCaptor;?? import?static?org.mockito.Mockito.*;?? ?? ? ? ? ? ?? public?class?PersonServiceTest?{?? ?? ????@Mock?? ????private?PersonDao?personDAO;???? ????private?PersonService?personService;???? ?? ????public?PersonServiceTest()?{?? ????}?? ?? ????@BeforeClass?? ????public?static?void?setUpClass()?{?? ????}?? ?? ????@AfterClass?? ????public?static?void?tearDownClass()?{?? ????}?? ?? ?????? ????@Before?? ????public?void?setUp()?throws?Exception?{?? ?????????? ????????MockitoAnnotations.initMocks(this);?? ?????????? ????????personService?=?new?PersonService(personDAO);?? ????}?? ?? ????@After?? ????public?void?tearDown()?{?? ????}?? ?? ????@Test?? ????public?void?shouldUpdatePersonName()?{?? ????????Person?person?=?new?Person(1,?"Phillip");?? ?????????? ????????when(personDAO.fetchPerson(1)).thenReturn(person);?? ?????????? ????????boolean?updated?=?personService.update(1,?"David");?? ?????????? ????????assertTrue(updated);?? ?????????? ????????verify(personDAO).fetchPerson(1);?? ?????????? ????????ArgumentCaptor<Person>?personCaptor?=?ArgumentCaptor.forClass(Person.class);?? ?????????? ????????verify(personDAO).update(personCaptor.capture());?? ?????????? ????????Person?updatePerson?=?personCaptor.getValue();?? ?????????? ????????assertEquals("David",?updatePerson.getPersonName());?? ?????????? ?????????? ????????verifyNoMoreInteractions(personDAO);?? ????}?? ?? ????@Test?? ????public?void?shouldNotUpdateIfPersonNotFound()?{?? ?????????? ????????when(personDAO.fetchPerson(1)).thenReturn(null);?? ?????????? ????????boolean?updated?=?personService.update(1,?"David");?? ?????????? ????????assertFalse(updated);?? ?????????? ????????verify(personDAO).fetchPerson(1);?? ?????????? ????????verifyZeroInteractions(personDAO);?? ?????????? ????????verifyNoMoreInteractions(personDAO);?? ????}?????? ?? ????? ? ?? ????@Test?? ????public?void?testUpdate()?{?? ????????System.out.println("update");?? ????????Integer?personId?=?null;?? ????????String?name?=?"Phillip";?? ????????PersonService?instance?=?new?PersonService(new?PersonDao()?{?? ?? ????????????@Override?? ????????????public?Person?fetchPerson(Integer?personID)?{?? ????????????????System.out.println("Not?supported?yet.");?? ????????????????return?null;?? ????????????}?? ?? ????????????@Override?? ????????????public?void?update(Person?person)?{?? ????????????????System.out.println("Not?supported?yet.");?? ????????????}?? ????????});?? ????????boolean?expResult?=?false;?? ????????boolean?result?=?instance.update(personId,?name);?? ????????assertEquals(expResult,?result);?? ?????????? ????????fail("The?test?case?is?a?prototype.");?? ????}?? }?? 這里setUpClass()、tearDownClass()、setUp()、tearDown()稱為測試夾具(Fixture),就是測試運行程序(test runner)在運行測試方法之前進行初始化、或之后進行回收資源的工作。JUnit 4之前是通過setUp、tearDown方法完成。在JUnit 4中,仍然可以在每個測試方法運行之前初始化字段和配置環境,當然也是通過注解完成。在JUnit 4中,通過@Before標注setUp方法;@After標注tearDown方法。在一個測試類中,甚至可以使用多個@Before來注解多個方法,這些方法都是在每個測試之前運行。說明一點,一個測試用例類可以包含多個打上@Test注解的測試方法,在運行時,每個測試方法都對應一個測試用例類的實例。
@Before是在每個測試方法運行前均初始化一次,同理@Ater是在每個測試方法運行完畢后均執行一次。也就是說,經這兩個注解的初始化和注銷,可以保證各個測試之間的獨立性而互不干擾,它的缺點是效率低。另外,不需要在超類中顯式調用初始化和清除方法,只要它們不被覆蓋,測試運行程序將根據需要自動調用這些方法。超類中的@Before方法在子類的@Before方法之前調用(與構造函數調用順序一致),@After方法是子類在超類之前運行。
這里shouldUpdatePersonName()、shouldNotUpdateIfPersonNotFound()和testUpdate()都是測試PersonService的update()方法,它依賴于PersonDao接口。前兩者使用了模擬測試。testUpdate()則沒有使用模擬測試。下面是測試結果:
圖4 測試結果點擊打開鏈接
可以看出,使用模擬測試的兩個測試成功了,沒有使用模擬測試的testUpdate()失敗。對于模擬測試,在測試用例類中要先聲明依賴的各個模擬對象,在setUp()中用MockitoAnnotations.initMocks()初始化所有模擬對象。在進行模擬測試時,要先設置模擬對象上方法的返回預期值,執行測試時會調用模擬對象上的方法,因此要驗證這些方法是否被調用,并且傳入的參數值是否符合預期。對于testUpdate()測試,我們需要自己創建測試PersonService.update()所需的所有PersonDao數據,因為我們只知道公開的PersonDao接口,其具體實現類(比如從數據庫中拿真實的數據,或寫入到數據庫中)可能由另一個團隊在負責,以適配不同的數據庫系統。這樣的依賴關系無疑使單元測試比較麻煩,而要拿真正PersonDao實現來進行測試,那也應該是后期集成測試的任務,把不同的組件集成到一起在真實環境中測試。有了模擬測試框架,就可以最大限度地降低單元測試時的依賴耦合性。
關注點?
?+ Mock框架是什么.
?+ 為什么要在測試中使用Mockito.?
參考
http://java.dzone.com/articles/the-concept-mocking??
http://en.wikipedia.org/wiki/Mockito?
http://code.google.com/p/mockito
本文翻譯自:http://www.codeproject.com/Articles/516360/Mockito-a-great-mock-framework-for-Java-developmen?,并做了一些修改。
from:?http://blog.csdn.net/zhoudaxia/article/details/33056093
總結
以上是生活随笔為你收集整理的Mockito:一个强大的用于Java开发的模拟测试框架的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。