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


原文鏈接:http://www.cnblogs.com/zishi/p/6780719.html

在實際項目中寫單元測試的過程中我們會發現需要測試的類有很多依賴,這些依賴項又會有依賴,導致在單元測試代碼里幾乎無法完成構建,尤其是當依賴項尚未構建完成時會導致單元測試無法進行。為了解決這類問題我們引入了Mock的概念,簡單的說就是模擬這些需要構建的類或者資源,提供給需要測試的對象使用。業內的Mock工具有很多,也已經很成熟了,這里我們將直接使用最流行的Mockito進行實戰演練,完成mockito教程。

Mock工具概述

1.1  Mockito簡介

 

EasyMock 以及 Mockito 都因為可以極大地簡化單元測試的書寫過程而被許多人應用在自己的工作中,但是這兩種 Mock 工具都不可以實現對靜態函數、構造函數、私有函數、Final 函數以及系統函數的模擬,但是這些方法往往是我們在大型系統中需要的功能。

另外,關於更多Mockito2.0新特性,參考官方介紹文檔,里邊有關於為什么不mock private的原因,挺有意思的:

https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2

1.2 Mockito准備工作

###Maven###

通過Maven管理的,需要在項目的Pom.xml中增加如下的依賴:

復制代碼
 <dependencies>

<dependency>

<groupId>org.mockito</groupId>

<artifactId>mockito-core</artifactId>

<version>2.7.19</version>

<scope>test</scope>

</dependency>

</dependencies>
復制代碼

 

 

在程序中可以import org.mockito.Mockito,然后調用它的static方法。

Maven用戶可以聲明對mockito-core的依賴。 Mockito自動發布到Bintray的中心,並同步到Maven Central Repository。

特別提醒:使用手工依賴關系管理的Legacy構建可以使用1. *“mockito-all”分發。 它可以從Mockito的Bintray存儲庫或Bintray的中心下載。 在但是Mockito 2. * “mockito-all”發行已經停止,Mockito 2以上版本使用“mockito-core”。

官網下載中心:

http://search.maven.org/#search|gav|1|g%3A%22org.mockito%22%20AND%20a%3A%22mockito-core%22

目前最新版本為2.7.19,由於公司網絡網關問題,最好是去官網手工下載。

另外Mockito需要Junit配合使用,在Pom文件中同樣引入:

復制代碼
<dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
      </dependency>
復制代碼

然后為了使代碼更簡潔,最好在測試類中導入靜態資源,還有為了使用常用的junit關鍵字,也要引入junit的兩個類Before和Test:

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;

1.3 模擬對象

創建 Mock 對象的語法為 mock(class or interface)。

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.4 設置對象調用的預期返回值

通過 when(mock.someMethod()).thenReturn(value) 來設定 Mock 對象某個方法調用時的返回值。我們可以看看源碼中關於thenReturn方法的注釋:

 

使用when(mock.someMethod()).thenThrow(new RuntimeException) 的方式來設定當調用某個方法時拋出的異常。

 

以及Answer:

 

 

Answer 是個泛型接口。到調用發生時將執行這個回調,通過  Object[] args = invocation.getArguments();可以拿到調用時傳入的參數,通過 Object mock = invocation.getMock();可以拿到mock對象。

有些方法可能接口的參數為一個Listener參數,如果我們使用Answer打樁,我們就可以獲取這個Listener,並且在Answer函數中執行對應的回調函數,這對我們了解函數的內部執行過成有很大的幫助。

使用doThrow(new RuntimeException(“clear exception”)).when(mockedList).clear();mockedList.clear();的方式Mock沒有返回值類型的函數:

doThrow(new RuntimeException()).when(mockedList).clear();

//將會 拋出 RuntimeException:

mockedList.clear();

這個實例表示當執行到mockedList.clear()時,將會拋出RuntimeException。其他的doXXX執行與它類似。

例如 : doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() 系列方法。

 

Spy函數:

你可以為真實對象創建一個監控(spy)對象,當你使用這個spy對象時,真實的對象也會被調用,除非它的函數被打樁。你應該盡量少的使用spy對象,使用時也需要小心,例如spy對象可以用來處理遺留代碼,Spy示例如下:

復制代碼
List list = new LinkedList();

//監控一個真實對象

List spy = spy(list);

//你可以為某些函數打樁

when(spy.size()).thenReturn(100);

//使用這個將調用真實對象的函數

spy.add("one");

spy.add("two");

//打印"one"

System.out.println(spy.get(0));

//size() 將打印100

System.out.println(spy.size());

//交互驗證

verify(spy).add("one"); 
verify(spy).add("two");
復制代碼

理解監控真實對象非常重要,有時,在監控對象上使用when(Object)來進行打樁是不可能或者不切實際的。因為,當使用監控對象時,請考慮用doReturn、Answer、Throw()函數組來進行打樁,例如:

List list = new LinkedList();

List spy = spy(list);

//這是不可能的: 因為調用spy.get(0)時會調用真實對象的get(0)函數,此時會發生

//IndexOutOfBoundsException異常,因為真實對象是空的 when(spy.get(0)).thenReturn("foo");

//你需要使用 doReturn() 來打樁

doReturn("foo").when(spy).get(0);

Mockito並不會為真實的對象代理函數調用,實際上它會復制真實對象,因此,如果你保留了真實對象並且與之交互,不要期望監控對象得到正確的結果。當你在監控對象上調用一個沒有stub函數時,並不會調用真實對象的對應函數,你不會在真實對象上看到任何效果。

1.5 驗證被測試類方法

Mock 對象一旦建立便會自動記錄自己的交互行為,所以我們可以有選擇的對它的 交互行為進行驗證。在 Mockito 中驗證 Mock 對象交互行為的方法是 verify(mock).someMethod(…)。最后 Assert() 驗證返回值是否和預期一樣。

1.6 Demo

 從網上找來一個最簡單的代碼實例,下面以具體代碼演示如何使用Mockito,代碼有三個類,分別如下:

Person類:

復制代碼
public class Person {
    private final int id; 
    private final String name; 
    public Person(int id, String name) { 
        this.id = id; 
        this.name = name; 
        } 
    public int getId() { 
        return id; 
        } 
    public String getName() { 
        return name; 
        }
}
復制代碼

PersonDao類:

public interface PersonDao {
    Person getPerson(int id); 
    boolean update(Person person); 
    }

PersonService類:

 

復制代碼
public class PersonService {
    private final PersonDao personDao; 
    public PersonService(PersonDao personDao) { 
        this.personDao = personDao; 
        } 
    public boolean update(int id, String name) { 
        Person person = personDao.getPerson(id); 
        if (person == null) 
        { return false; } 
        Person personUpdate = new Person(person.getId(), name); 
        return personDao.update(personUpdate); 
        }
}
復制代碼

 

仍然使用Junit自動生成測試類或者手工新建測試類:

 

 

測試代碼生成后,將默認assertfail的刪掉,輸入以下兩個測試方法:

復制代碼
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;


public class PersonServiceTest {

    private PersonDao     mockDao;
    private PersonService personService;

    @Before
    public void setUp() throws Exception {
        //模擬PersonDao對象
        mockDao = mock(PersonDao.class);
        when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
        when(mockDao.update(isA(Person.class))).thenReturn(true);

        personService = new PersonService(mockDao);
    }

    @Test
    public void testUpdate() throws Exception {
        boolean result = personService.update(1, "new name");
        assertTrue("must true", result);
        //驗證是否執行過一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));
        //驗證是否執行過一次update
        verify(mockDao, times(1)).update(isA(Person.class));
    }

    @Test
    public void testUpdateNotFind() throws Exception {
        boolean result = personService.update(2, "new name");
        assertFalse("must true", result);
        //驗證是否執行過一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));
        //驗證是否執行過一次update
        verify(mockDao, never()).update(isA(Person.class));
    }
}
復制代碼

注意:我們對PersonDAO進行mock,並且設置stubbing,stubbing設置如下:

  • 當getPerson方法傳入1的時候,返回一個Person對象,否則默認返回空
  • 當調update方法的時候,返回true

這里使用了參數匹配器:

isA():Object argument that implements the given class.

eq():int argument that is equal to the given value

注:參數匹配的概念這里不再展開敘述。

 

仍然調用Junit執行單元測試代碼,結果如圖所示:

驗證了兩種情況:

  • 更新id為1的Person的名字,預期:能在DAO中找到Person並更新成功
  • 更新id為2的Person的名字,預期:不能在DAO中找到Person,更新失敗

這里也可以查看Eclipse拋出的異常信息:

Argument(s) are different! Wanted:

personDao.getPerson(1);

-> at PersonServiceTest.testUpdateNotFind(PersonServiceTest.java:41)

Actual invocation has different arguments:

personDao.getPerson(2);

-> at PersonService.update(PersonService.java:8)

2 單元測試與覆蓋率

1、Junit 2、JaCoCo 3、EclEmma

2 覆蓋率

覆蓋率如下圖顯示:

覆蓋率仍然使用JaCoCo和EclEmma:

l  未覆蓋代碼標記為紅色

l  已覆蓋代碼會標記為綠色

l  部分覆蓋的代碼標記為黃色

顏色也可以在Eclipse中自定義設置:

在Eclipse下方的狀態欄窗口,有一欄“Coverage”,點擊可以顯示詳細的代碼覆蓋率:

如何導出為Html格式的Report:

在Eclipse下方的Coverage欄鼠標右鍵選擇“Export Session…”,在彈出窗口中選擇export的目標為“Coverage Report”如下圖:

點擊“Next”按鈕后,在接下來的彈出窗口選擇需要導出的session,Format

類型選擇“HTML report”,導出位置暫時選擇為桌面,都選擇之后點擊“Finish”按鈕就生成好了。

在桌面上找到一個叫做index.html的頁面就是剛剛生成好的Coverage Report:

點擊文件夾可以進入目錄,進一步查看子文件的覆蓋率:

 

 

附錄:參考文檔一覽

 

Mockito官網: http://site.mockito.org/

5分鍾了解Mockito:http://liuzhijun.iteye.com/blog/1512780

Mockito簡單介紹及示例:http://blog.csdn.net/huoshuxiao/article/details/6107835

Mockito淺談:http://www.jianshu.com/p/77db26b4fb54

單元測試利器-Mockito 中文文檔:http://blog.csdn.net/bboyfeiyu/article/details/52127551

Mockito使用指南 :http://blog.csdn.net/shensky711/article/details/52771493

JUnit+Mockito 單元測試(二):http://blog.csdn.net/zhangxin09/article/details/42422643

 

感謝閱讀!作者原創技術文章,轉載請注明出處

 

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

 

單元測試系列:JUnit單元測試規范

 

單元測試系列:Mock工具Jmockit使用介紹

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM