看完點個贊唄,難道想白嫖不成?更多內容請訪問微信公眾號 :三國測,掃碼關注喲!
原文鏈接:http://www.cnblogs.com/zishi/p/6760272.html
Mock工具Jmockit使用介紹
在寫單元測試的過程中我們會發現需要測試的類有很多依賴,這些依賴的類或者資源又會有依賴,導致在單元測試代碼里無法完成構建,我們應對的方法是Mock。簡單的說就是模擬這些需要構建的類或者資源,提供給需要測試的對象使用。
1、Mock工具概述
1.1 mock工具列表
可用的Mock Toolkit有許多,比較常見的有EasyMock, Jmock和JMockit等等,到底選哪個呢,Jmockit的官網上有個特性對比列表,很詳細,他們的功能對比如下:
Feature |
PowerMock: |
PowerMock: |
|||||
√ |
√ |
√ |
√ |
√ |
√ |
||
√ |
√ |
√ |
√ |
||||
√ |
√ |
√ |
√ |
||||
√ |
√ |
√ |
√ |
√ |
√ |
||
√ |
√ |
√ |
√ |
||||
N/A |
N/A |
N/A |
√ |
||||
No extra "prepare for test" code |
√ |
√ |
√ |
√ |
√ |
||
No need to use @RunWith annotation or base |
√ |
√ |
√ |
√ |
|||
Consistent syntax between void and non-void methods |
√ |
√ |
√ |
||||
Argument matchers for some parameters only, |
√ |
√ |
|||||
Easier argument matching based on properties |
√ |
√ |
√ |
√ |
√ |
√ |
|
√ |
√ |
√ |
√ |
||||
√ |
√ |
√ |
|||||
√ |
√ |
√ |
√ |
√ |
|||
√ |
√ |
||||||
Mocking of constructors and final/static/native/private methods |
√ |
√ |
√ |
||||
Declarative application of mocks/stubs to |
√ |
√ |
√ |
||||
√ |
√ |
√ |
√ |
||||
Mocking of "new-ed" objects |
√ |
√ |
√ |
||||
√ |
√ |
√ |
|||||
Declarative mocks for the test class (mock |
√ |
√ |
√ |
√ |
√ |
||
Declarative mocks for test methods |
√ |
||||||
√ |
|||||||
√ |
|||||||
Use of special fields to specify invocation |
√ |
||||||
√ |
|||||||
√ |
|||||||
√ |
|||||||
√ |
|||||||
√ |
|||||||
√ |
|||||||
Single jar file in the classpath is sufficient to |
√ |
N/A |
N/A |
√ |
|||
Total |
6/32 |
7/32 |
13/31 |
11/31 |
9/31 |
14/30 |
32/32 |
Total when ignoring JMockit-only features |
6/22 |
7/22 |
13/21 |
11/21 |
9/21 |
14/20 |
22/22 |
1.2 Mockito簡介
EasyMock 以及 Mockito 都因為可以極大地簡化單元測試的書寫過程而被許多人應用在自己的工作中,但是這兩種 Mock 工具都不可以實現對靜態函數、構造函數、私有函數、Final 函數以及系統函數的模擬,但是這些方法往往是我們在大型系統中需要的功能。
關於更多Mockito2.0新特性,參考官方介紹文檔,里邊有關於為什么不mock private的原因,挺有意思的:
https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2
Mockito 的使用
###Maven###
通過Maven管理的,需要在項目的Pom.xml中增加如下的依賴:
<dependencies> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.0</version> <scope>test</scope> </dependency> </dependencies>
在程序中可以import org.mockito.Mockito,然后調用它的static方法。
1.2.1 模擬對象
創建 Mock 對象的語法為 mock(class or interface)。
1.2.2 設置對象調用的預期返回值
通過 when(mock.someMethod()).thenReturn(value) 來設定 Mock 對象某個方法調用時的返回值。或者使用 when(mock.someMethod()).thenThrow(new RuntimeException) 的方式來設定當調用某個方法時拋出的異常。
1.2.3 驗證被測試類方法
Mock 對象一旦建立便會自動記錄自己的交互行為,所以我們可以有選擇的對它的 交互行為進行驗證。在 Mockito 中驗證 Mock 對象交互行為的方法是 verify(mock).someMethod(…)。最后 Assert() 驗證返回值是否和預期一樣。
1.2.4 Demo
Mock 對象的創建
mock(Class classToMock);
mock(Class classToMock, String name)
mock(Class classToMock, Answer defaultAnswer)
mock(Class classToMock, MockSettings mockSettings)
mock(Class classToMock, ReturnValues returnValues)
可以對類和接口進行mock對象的創建,創建時可以為mock對象命名。對mock對象命名的好處是調試的時候容易辨認mock對象。
Mock對象的期望行為和返回值設定
假設我們創建了LinkedList類的mock對象:
LinkedList mockedList = mock(LinkedList.class);
1.3 PowerMock簡介
PowerMock 是在 EasyMock 以及 Mockito 基礎上的擴展,通過定制類加載器等技術,PowerMock 實現了之前提到的所有模擬功能,使其成為大型系統上單元測試中的必備工具。缺點是缺少文檔。
mock是模擬對象,用於模擬真實對象的行為。
Powermock主要用於打樁。比如:方法A的參數需要傳入實例B,方法A需要調用B的某個方法B.C()。方法C因為耗時長或者根本沒有實現或者其他不方便在單元測試中實現等原因,需要偽造返回,此時Powermock即可派上用場。
PowerMock擴展了EasyMock和Mockito框架,增加了對static和final方法mock支持等功能。這里主要基於PowerMock Mockito API進行介紹。
PowerMock支持JUnit和TestNG,這里基於JUnit。
安裝
下載地址:https://github.com/jayway/powermock/wiki/Downloads。下載" Mockito and JUnit including dependencies"版本。當前版本為”powermock-mockito-junit-1.6.3.zip"。
1.4 Stub和Mock
###Mock###
所謂的mock,即模擬,模仿的意思。Mock 技術的主要作用是使用mock工具模擬一些在應用中不容易構造或者比較復雜的對象,從而把測試目標與測試邊界以外的對象隔離開。
###Stub###
Stub,樁。單元測試過程中,對於在應用中不容易構造或者比較復雜的對象,用一個虛擬的對象來代替它。從類的實現方式上看,stub有一個顯式的類實現,按照stub類的復用層次可以實現為普通類(被多個測試案例復用),內部類(被同一個測試案例的多個測試方法復用)乃至內部匿名類(只用於當前測試方法)。stub的方法也會有具體的實現,哪怕簡單到只有一個簡單的return語句。
###Stub 與 Mock 的區別###
Stub 是在單元測試過程中去代替某些對象來提供所需的測試數據,適用於基於狀態的(state-based)測試,關注的是輸入和輸出。而Mock適用於基於交互的(interaction-based)測試,關注的是交互過程,不只是模擬狀態,還能夠模擬模塊或對象的行為邏輯並能驗證其正確性,Mock不需要類的顯示實現,直接用工具模擬。
2、Jmockit安裝
綜合考量下來,所以我們的mock工具也選擇了jmockit(http://jmockit.org/index.html)
關於如何使用:
推薦:在Maven 的pom.xml文件中添加以下依賴節點:
<dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.30</version> <scope>test</scope> </dependency>
當然也可以在項目中直接引入jar包。
3、如何創建一個 mock 對象
JMockit模擬API可用於JUnit 4(版本4.5或更高版本),JUnit 5或TestNG(版本6.2或更高版本)編寫測試。 現在讓我們看看這個API是如何進行模擬的,為了便於理解,下面引用官網給出的范例代碼,同時我們將必要的類庫進行引用。
在測試類中,聲明一個你想要模擬的類型的mock字段,並用@Mocked,@Injectable或@Capturing注釋。當模擬類時,@Injectable意味着只有被分配mock字段的實例將具有mock行為; 否則,被mock的類的所有實例將被mock。
import org.junit.*; import mockit.*; public class MyFirstJMockitTest { // Mocked實例(而不是常規的“mock對象”)將自動創建並分配到帶注釋的mock字段 @Mocked Collaborator mock1; //所有當前和未來的實例都會被mock @Injectable AnotherDependency anotherMock; //只有一個特定實例被mock @Test public void myFirstTestMethod() { //任何mock字段都可以在這里或者類的任何其他測試方法中使用 } @Test public void testMethodWithMockParameter(@Mocked YetAnotherDependency testSpecificMock) { ... } ... }
注意:上面的測試類顯示了一些不同的東西:第二個測試方法聲明一個參數! 通常,JUnit / TestNG測試方法不允許有參數。 然而,當使用JMockit時,允許這樣的模擬參數。 一般來說,只有測試類中大多數或所有測試都需要Mock類型時,才使用測試類的Mock字段。 否則,Mock的范圍最好僅限於單個測試的Mock參數。 JMockit將總是關注實例化Mock類型,並且當測試運行器調用測試方法時,將實例分配給mock字段(假設字段不是final)或將其作為參數傳遞。
4、Mock范例1
要Mock測試的方法如下:
public class MyObject { public String hello(String name){ return "Hello " + name; } }
使用JMockit編寫的單元測試如下:
@Mocked //用@Mocked標注的對象,不需要賦值,jmockit自動mock MyObject obj; @Test public void testHello() { new NonStrictExpectations() {//錄制預期模擬行為 { obj.hello("Zhangsan"); returns("Hello Zhangsan"); //也可以使用:result = "Hello Zhangsan"; } }; assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//調用測試方法 new Verifications() {//驗證預期Mock行為被調用 { obj.hello("Hello Zhangsan"); times = 1; } }; }
代碼完成后,運行單元測試,結果如下:
JMockit也可以分類為非局部模擬與局部模擬,區分在於Expectations塊是否有參數,有參數的是局部模擬,反之是非局部模擬。
而Expectations塊一般由Expectations類和NonStrictExpectations類定義,類似於EasyMock和PowerMock中的Strict Mock和一般性Mock。
用Expectations類定義的,則mock對象在運行時只能按照 Expectations塊中定義的順序依次調用方法,不能多調用也不能少調用,所以可以省略掉Verifications塊;
而用NonStrictExpectations類定義的,則沒有這些限制,所以如果需要驗證,則要添加Verifications塊。
上述的例子使用了非局部模擬,下面我們使用局部模擬來改寫上面的測試,代碼如下:
@Test public void testHello() { final MyObject obj = new MyObject(); new NonStrictExpectations(obj) {//錄制預期模擬行為 { obj.hello("Zhangsan"); returns("Hello Zhangsan"); //也可以使用:result = "Hello Zhangsan"; } }; assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//調用測試方法 new Verifications() {//驗證預期Mock行為被調用 { obj.hello("Hello Zhangsan"); times = 1; } }; }
模擬靜態方法:
@Test public void testMockStaticMethod() { new NonStrictExpectations(ClassMocked.class) { { ClassMocked.getDouble(1);//也可以使用參數匹配:ClassMocked.getDouble(anyDouble); result = 3; } }; assertEquals(3, ClassMocked.getDouble(1)); new Verifications() { { ClassMocked.getDouble(1); times = 1; } }; }
模擬私有方法:
如果ClassMocked類中的getTripleString(int)方法指定調用一個私有的multiply3(int)的方法,我們可以使用如下方式來Mock:
@Test public void testMockPrivateMethod() throws Exception { final ClassMocked obj = new ClassMocked(); new NonStrictExpectations(obj) { { this.invoke(obj, "multiply3", 1);//如果私有方法是靜態的,可以使用:this.invoke(null, "multiply3") result = 4; } }; String actual = obj.getTripleString(1); assertEquals("4", actual); new Verifications() { { this.invoke(obj, "multiply3", 1); times = 1; } }; }
5、Mock案例2
接下來我們用Jmockit實現一個具體的單元測試,首先下面是一段Controller的功能代碼:
import com.odde.mail.model.Result; import com.odde.mail.service.MailService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import static java.lang.String.format; @Controller @RequestMapping("/mail") public class MailController { private static final Log log = LogFactory.getLog(MailController.class); private final ObjectMapper mapper = new ObjectMapper(); @Autowired private MailService mailService; @RequestMapping(value = "/send", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8") public @ResponseBody String send(@RequestParam("recipients") String recipients, @RequestParam("subject") String subject, @RequestParam("content") String content) throws Exception { log.debug("mail controller send start"); log.debug(format("recipients:%s", recipients)); log.debug(format("subject:%s", subject)); log.debug(format("content:%s", content)); Result mailResult = mailService.send(recipients, subject, content); String result = mapper.writeValueAsString(mailResult); log.debug(format("result:%s", result)); log.debug("mail controller send finish"); return result; } }
接下來我們看一下Jmockit實現的具體的單元測試代碼:
import com.odde.mail.model.Result; import com.odde.mail.service.MailService; import mockit.Expectations; import mockit.Injectable; import mockit.Tested; import mockit.integration.junit4.JMockit; import org.junit.Test; import org.junit.runner.RunWith; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @RunWith(JMockit.class) public class MailControllerTest { @Tested MailController mailController; @Injectable private MailService mailService; @Test public void should_return_status_success_when_send_mail_success() throws Exception { new Expectations() { { mailService.send("test@test.com", "test", "test"); result = new Result("成功"); } }; String result = mailController.send("test@test.com", "test", "test"); assertThat(result, is("{\"status\":\"成功\"}")); } }
- @RunWith(JMockit.class): 指定單元測試的執行類為JMockit.class;
- @Tested: 這個是指被測試類,在這個測試案例中我們要測試的是MailController,所以我們給其打上這個標簽;
- @Injectable: 這個可以將對象進行mock並自動關聯到被測試類,而不需要通過其他文件類似spring的配置文件等來進行關聯;
- @Expectations: mock對象mailService的send方法,讓其返回一個Result對象;
做完上面這些基本就可以了,后面的被測方法調用和驗證都跟原來的一樣。這樣看起來是不是比原來的單元測試代碼少了一些,也更簡潔了一些,最重要的一點是這樣的單元測試不依賴spring的bean定義文件,不需要啟動web服務,執行起來速度很快。
6、Mock案例3
首先仍然是先看一下Service的功能代碼,代碼也比較簡單,就是調用Repository做一些增刪改查的動作。
import com.odde.mail.model.Recipient; import com.odde.mail.model.Result; import com.odde.mail.repo.RecipientRepository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class RecipientService { @Autowired private RecipientRepository recipientRepository; public Result add(String username, String email) { Recipient recipient = recipientRepository.findByEmail(email); Result result; if (recipient == null) { recipientRepository.save(new Recipient(username, email)); result = new Result("成功"); } else { result = new Result("失敗"); } return result; } }
接着就是它的單元測試代碼,我們看一下Jmockit如何實現的mock代碼:
import com.odde.mail.model.Recipient; import com.odde.mail.model.Result; import com.odde.mail.repo.RecipientRepository; import mockit.Injectable; import mockit.NonStrictExpectations; import mockit.Tested; import mockit.integration.junit4.JMockit; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; import static java.util.Arrays.asList; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; @RunWith(JMockit.class) public class RecipientServiceTest { @Tested private RecipientService recipientService; @Injectable private RecipientRepository recipientRepository; @Test public void should_return_success_when_add_recipient_not_exist() throws Exception { Result result = recipientService.add("Tom", "test@test.com"); assertThat(result.getStatus(), is("成功")); } }
相對Controller Test這里少了一步對recipientRepository對象findByEmail方法的mock,因為如果不通過Expectations進行方法mock的話,方法會默認返回null,而我們要測試的場景正是需要findByEmail方法返回null,所以mock方法這一步我們也省了。改寫后的整體代碼也比原來的少了很多,而且速度更快。
7、Mock使用建議
JMockit功能非常強大,不僅可以輕松處理上面的這些測試場景,還可以對static,final,private等方法進行mock,可以讓你的單元測試毫無阻礙的進行。
但是如果過度的使用Mock框架,會讓功能代碼的真正問題被掩蓋。本來單元測試的設計可以讓你發現功能代碼上的一些設計是否合理,比如有沒有緊耦合等,但使用JMockit可以讓你在設計不合理的代碼上也可以輕松地進行單元測試,這樣你就很難發現功能代碼上的問題了。
所以建議JMockit等類似的mock框架還是要謹慎使用,首先要保證功能代碼設計合理,滿足面向對象設計的要求,再來考慮提高單元測試效率的問題。
另外,Mock的函數是不計算到單元測試覆蓋率里邊的,如下圖所示:
注意:我們mock了hello方法,但是在EclEmma中顯示覆蓋率為0%。
8、重要:注意事項以及調試中遇到的問題
問題1 編譯報錯方法名無法找到
在pom.xml文件中,注意依賴次序,JMockit一定要在JUnit之前,否則容易出現編譯報錯方法名找不到之類的奇葩問題:
問題2 報錯Jmockit初始化異常
引用的版本必須保持一致,可以在build path里查看是否是自己用的版本,否則會報錯Jmockit初始化異常:
問題3 版本造成java.lang.NoSuchMethodError
Junit使用最新版本是4.12。系統默認的junit版本太低了。會報異常如下:
java.lang.NoSuchMethodError: org.junit.runner.Request.classWithoutSuiteMethod(Ljava/lang/Class;)Lorg/junit/runner/Request;
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createFilteredTest(JUnit4TestLoader.java:76)
at org.eclipse.jdt.internal.
改為最新版本,Junit運行正常:
<!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
問題4 報錯cannot be resolved to a type
調試過程中報錯:
xxx cannot be resolved to a type
解決方法,在發生錯誤的項目上單擊鼠標右鍵-〉Properties,選中“Resource”,右側Text file encoding選擇“Other:UTF-8”,點擊“Apply”按鈕。
附錄:參考文檔一覽
JMockit官網:http://jmockit.org/
使用JMockit編寫java單元測試: http://blog.csdn.net/chjttony/article/details/17838693
感謝閱讀,作者原創技術文章,轉載請注明出處
看完點個贊唄,難道想白嫖不成?更多內容請訪問微信公眾號 :三國測,掃碼關注喲!
其他推薦相關閱讀:
單元測試系列之一:如何使用JUnit、JaCoCo和EclEmma提高單元測試覆蓋率
單元測試系列之四:Sonar平台中項目主要指標以及代碼壞味道詳解
單元測試系列之七:Sonar 數據庫表關系整理一(rule相關)