單元測試系列之二:Mock工具Jmockit實戰

更多原創測試技術文章同步更新到微信公衆號 :三國測,敬請掃碼關注我的的微信號,感謝!html

 

原文連接:http://www.cnblogs.com/zishi/p/6760272.htmljava

Mock工具Jmockit使用介紹git

 

在寫單元測試的過程當中咱們會發現須要測試的類有不少依賴,這些依賴的類或者資源又會有依賴,致使在單元測試代碼裏沒法完成構建,咱們應對的方法是Mock。簡單的說就是模擬這些須要構建的類或者資源,提供給須要測試的對象使用。github

一、Mock工具概述

1.1 mock工具列表

可用的Mock Toolkit有許多,比較常見的有EasyMock, Jmock和JMockit等等,到底選哪一個呢,Jmockit的官網上有個特性對比列表,很詳細,他們的功能對好比下:web

 

Featurespring

EasyMock數據庫

jMockapache

Mockito微信

Unitils Mockapp

PowerMock:
EasyMock
API

PowerMock:
Mockito
API

JMock
it

Invocation count constraints

 

Recording strict expectations

   

 

Explicit verification

   

 

Partial mocking

 

No method call to switch from record to replay

   

 

No extra code for implicit verification

   

N/A

N/A

 

N/A

No extra "prepare for test" code

   

No need to use @RunWith annotation or base
test class

     

Consistent syntax between void and non-void methods

 

 

   

Argument matchers for some parameters only,
not all

     

   

Easier argument matching based on properties
of value objects

 

Cascading mocks

   

 

Support for mocking multiple interfaces

   

   

Support for mocking annotation types

 

 

Partially ordered expectations

 

       

Mocking of constructors and final/static/native/private methods

       

Declarative application of mocks/stubs to
whole test classes

       

Auto-injection of mocks

   

 

Mocking of "new-ed" objects

       

Support for mocking enum types

       

Declarative mocks for the test class (mock
fields)

   

Declarative mocks for test methods
(parameters, local fields)

           

Special fields for "any" argument matching

           

Use of an special field to specify invocation
results

           

Use of special fields to specify invocation
count constraints

           

Expectations with custom error messages

           

On-demand mocking of unspecified implementation classes

           

Capture of instances created by code under
test

           

Recording & verification of expectations in
loops

           

Support for covariant return types

           

"Duck typing" mocks for state-based tests

           

Single jar file in the classpath is sufficient to
use mocking API

   

 

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不須要類的顯示實現,直接用工具模擬。

 

二、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包。

三、如何建立一個 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)或將其做爲參數傳遞。

 

四、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;
        }
    };
}

 

五、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服務,執行起來速度很快。

 

六、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方法這一步咱們也省了。改寫後的總體代碼也比原來的少了不少,並且速度更快。

 

七、Mock使用建議

 

JMockit功能很是強大,不只能夠輕鬆處理上面的這些測試場景,還能夠對static,final,private等方法進行mock,可讓你的單元測試毫無阻礙的進行。
可是若是過分的使用Mock框架,會讓功能代碼的真正問題被掩蓋。原本單元測試的設計可讓你發現功能代碼上的一些設計是否合理,好比有沒有緊耦合等,但使用JMockit可讓你在設計不合理的代碼上也能夠輕鬆地進行單元測試,這樣你就很難發現功能代碼上的問題了。
因此建議JMockit等相似的mock框架仍是要謹慎使用,首先要保證功能代碼設計合理,知足面向對象設計的要求,再來考慮提升單元測試效率的問題。

另外,Mock的函數是不計算到單元測試覆蓋率裏邊的,以下圖所示:

 

 

 

 

 

注意:咱們mock了hello方法,可是在EclEmma中顯示覆蓋率爲0%。

 

八、重要:注意事項以及調試中遇到的問題

問題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提升單元測試覆蓋率

 

測試系列之二:Mock工具Jmockit實戰

 

單元測試系列之三:JUnit單元測試規範

 

單元測試系列之四:Sonar平臺中項目主要指標以及代碼壞味道詳解

 

單元測試系列之五:Mock工具之Mockito實戰

 

單元測試系列之六:JUnit5 技術前瞻

 

單元測試系列之七:Sonar 數據庫表關係整理一(rule相關)

 

單元測試系列之八:Sonar 數據庫表關係整理一(續)

 

單元測試系列之九:Sonar 經常使用代碼規則整理(一)

 

單元測試系列之十:Sonar 經常使用代碼規則整理(二)

 

單元測試系列之十一:Jmockit之mock特性詳解

相關文章
相關標籤/搜索