MockIto進行模擬單元測試

JUnit和Mockito

對於JUnit,這裏就不詳細介紹了,網上的教程有很多,比如這個這個

下面主要介紹一下Mockito

什麼是mock測試,什麼是mock對象?

先來看看下面這個示例:

從上圖可以看出如果我們要對A進行測試,那麼就要先把整個依賴樹構建出來,也就是BCDE的實例。

一種替代方案就是使用mocks

從圖中可以清晰的看出:

  • mock對象就是在調試期間用來作爲真實對象的替代品
  • mock測試就是在測試過程中,對那些不容易構建的對象用一個虛擬對象來代替測試的方法就叫mock測試

模擬的好處是什麼?

  • 提前創建測試; TDD(測試驅動開發)

  如果你創建了一個Mock那麼你就可以在service接口創建之前寫Service Tests了,這樣你就能在開發過程中把測試添加到你的自動化測試環境中了。換句話說,模擬使你能夠使用測試驅動開發。

  • 團隊可以並行工作

  這類似於上面的那點;爲不存在的代碼創建測試。但前面講的是開發人員編寫測試程序,這裏說的是測試團隊來創建。當還沒有任何東西要測的時候測試團隊如何來創建測試呢?模擬並針對模擬測試!這意味着當service藉口需要測試時,實際上QA團隊已經有了一套完整的測試組件;沒有出現一個團隊等待另一個團隊完成的情況。這使得模擬的效益型尤爲突出了。

  • 你可以創建一個驗證或者演示程序。

  • 爲無法訪問的資源編寫測試

  這個好處不屬於實際效益的一種,而是作爲一個必要時的“救生圈”。有沒有遇到這樣的情況?當你想要測試一個service接口,但service需要經過防火牆訪問,防火牆不能爲你打開或者你需要認證才能訪問。遇到這樣情況時,你可以在你能訪問的地方使用MockService替代,這就是一個“救生圈”功能。

  • Mock 可以分發給用戶

  • 隔離系統

知道什麼是mock測試後,那麼我們就來認識一下mock框架---Mockito。

Mockito區別於其他模擬框架的地方主要是允許開發者在沒有建立“預期”時驗證被測系統的行爲。

Mockito相關教程:

(與此同時推薦一個東西,SpringOckito, 不過已經2年沒更新了。)

mockito入門實例

Maven依賴:

Xml代碼

<dependencies>  
<dependency>  
<groupId>org.mockito</groupId>  
<artifactId>mockito-all</artifactId>  
<version>1.8.5</version>  
<scope>test</scope>  
</dependency>  
</dependencies>

首先,需要在@Before註解的setUp()中進行初始化(下面這個是個測試類的基類)

Java代碼

public abstract class MockitoBasedTest {
    @Before
    public void setUp() throws Exception {
        // 初始化測試用例類中由Mockito的註解標註的所有模擬對象
        MockitoAnnotations.initMocks(this);
    }
}

Java代碼

import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
public class SimpleTest {  

    @Test  
    public void simpleTest(){  

        //創建mock對象,參數可以是類,也可以是接口  
        List<String> list = mock(List.class);  

        //設置方法的預期返回值  
        when(list.get(0)).thenReturn("helloworld");  

        String result = list.get(0);  

        //驗證方法調用(是否調用了get(0))  
        verify(list).get(0);  

        //junit測試  
        Assert.assertEquals("helloworld", result);  
    }  
}

創建mock對象不能對final,Anonymous ,primitive類進行mock。

可對方法設定返回異常

Java代碼

when(list.get(1)).thenThrow(new RuntimeException("test excpetion"));

stubbing另一種語法(設置預期值的方法),可讀性不如前者

Java代碼

doReturn("secondhello").when(list).get(1);

沒有返回值的void方法與其設定(支持迭代風格,第一次調用donothing,第二次dothrow拋出runtime異常)

Java代碼

doNothing().doThrow(new RuntimeException("void exception")).when(list).clear();  
list.clear();  
list.clear();  
verify(list,times(2)).clear();

參數匹配器(Argument Matcher)

Matchers類內加你有很多參數匹配器 anyInt、anyString、anyMap.....Mockito類繼承於Matchers,Stubbing時使用內建參數匹配器,下例:

Java代碼

@Test  
public void argumentMatcherTest(){  
    List<String> list = mock(List.class);  

    when(list.get(anyInt())).thenReturn("hello","world");  

    String result = list.get(0)+list.get(1);  

    verify(list,times(2)).get(anyInt());  

    Assert.assertEquals("helloworld", result);  
}

需要注意的是:如果使用參數匹配器,那麼所有的參數都要使用參數匹配器,不管是stubbing還是verify的時候都一樣。

EclEmma

在衆多的Java覆蓋率測試工具中,開源的Emma是最著名的一個,而EclEmma相當於是它在Eclipse上的圖形化界面插件。它使用簡單,結果直觀。

安裝很簡單,打開Eclipse,點擊Help → Install New Software →輸入update.eclemma.org,安裝軟件即可。

首先,我們可以建立一個HelloWorld類,然後通過Coverage來運行它。

fig003.jpg

 

執行完畢之後,我們正在編輯 HelloWorld.java 的窗口將會變成如下所示:

fig004.jpg

 

EclEmma 用不同的色彩標示了源代碼的測試情況。其中,綠色的行表示該行代碼被完整的執行,紅色部分表示該行代碼根本沒有被執行,而黃色的行表明該行代碼部分被執行。黃色的行通常出現在單行代碼包含分支的情況。

EclEmma 還提供了一個單獨的視圖來統計程序的覆蓋測試率。可以選擇行覆蓋(Line),分支(Branch)覆蓋等多種覆蓋率檢測標準。

fig005.jpg

 

(更多資料,請參考:這裏這裏,和這裏)。

Spring與單元測試

首先在maven中加載以下的庫,尤其是第三個。

[庫] junit:4.12

[庫] mockito-core:1.10.19

[庫] spring-boot-starter-test:1.3.1

Pom.xml (節選)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

接下來,如果你要用到一些Spring自帶的註解,比如@Autowired的話,最好是在測試類的基類中,加入如下註解,這樣會使得測試時先將SpringBoot運行起來。

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)

接下來需要在@Before註解的setUp()中進行初始化

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
public abstract class MockitoBasedTest {
    @Before
    public void setUp() throws Exception {
        // 初始化測試用例類中由Mockito的註解標註的所有模擬對象
        MockitoAnnotations.initMocks(this);
    }
}

由於Eclipse對於import static的支持很差,你可能還需要記得加上

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

接下來我們爲每個類創建測試用例,在比如Service的一個類上面右鍵-新建-JUnit Test Case,注意要把測試類的目錄改到src/test/java

至於其他的部分,與上文中提到的mockito的測試步驟基本相同。至於涉及到@Autowired這種,涉及Spring框架的註解而導致測試無法順利進行的問題,請看下一節的講解。

相關鏈接:

測試中遇到的問題和解決辦法

1) 被測試類、測試類的覆蓋率不同

微信截圖_20160109224030.png

 

我們以WeChatServiceImpl類和它的測試類爲例,WeChatServiceImpl的代碼不變,爲了達到上圖的效果,我們把測試函數中內容刪掉:

@Test
public void testIsOutOfTime() {
    ;
}

因此可以看出,

  • 100%是代表測試函數的每一行都成功執行了,比如我只輸入一個分號;

  • 但是,4.2%才代表的是被測函數的實際覆蓋率

所以不要被測試類覆蓋率的100%騙了~stuck_out_tongue

2) 被測類中@Autowired註解,如何控制其中Repository返回值

public class GameHelper {
    @Autowired
    private PointRepository pointRepository;

    public boolean checkLineItem(final Line line) {
        Point fromPoint = pointRepository.findById(line.getFromPointId());  //如何控制這個repository的返回?
        Point toPoint = pointRepository.findById(line.getToPointId());
        return fromPoint.getID().equals(toPoint.getID());//簡化了原函數
    }
    ...
}

因爲在目前的單元測試中,Spring一個很特殊的註解是@Autowired。(@Autowired可以對成員變量、方法和構造函數進行標註,來完成自動裝配的工作。)

如果我們要寫個testCheckLineItem()函數的話,我們怎麼控制fromPoint和toPoint呢?

因爲不能改變被測類,因此我曾經嘗試在測試類中使用過以下方法:

  • 在測試函數中,使用new PointRepository().tostory

  • 對pointRepository使用@Mock,@Spy,@InjectMocks等

  • 對pointRepository加@Autowired註解,然後發現註解無效。於是在所有測試類的基類中,增加如下註解以啓用@Autowired,但是依然有問題。(不過如果想使用@Autowired一類的註解,下面這個代碼是必須加的。)

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @SpringApplicationConfiguration(classes = Application.class)
    
  • 後來與@Autowired一起加了@Spy也不行!!

  • 以及對@Mock,@Spy,@InjectMocks的各種使用組合...

最後,經過朋友的提示以及查找,我查到了這個問題的英文解釋和中文的解答

正確答案是對被測類中@Autowired的對象,用@Mocks標註;對被測類自己,用@InjectMocks標註。代碼如下:

public class GameHelperTest {
    @Mock
    private PointRepository pointRepository;

    @InjectMocks
    private GameHelper gamehelper;   //pointRepository作爲mock對象被注入到gamehelper中,gamehelper其他成員變量不變
    public void testCheckLineItem() {
        Line line = new Line(***);
        when(pointRepository.findById(123L)).thenReturn(new Point(***));
        when(pointRepository.findById(456L)).thenReturn(new Point(***));
        assertTrue(gamehelper.checkLineItem(line));
    }
    ...
}

至於原因,我們回到mockito的官方文檔中去看關於@InjectMocks的解釋。

@InjectMocks - injects mock or spy fields into tested object automatically.

換言之,@Mock標註的對象會自動注入到被@InjectMocks標註的對象中。比如在本例中,GameHelper中的成員變量pointRepository(的函數),就會被我們用在測試用例中改寫過返回值的pointRepository對象替換掉。

另外,經測試,thenReturn返回的是對象引用而不是深複製了對象本身(所以可以減少寫thenReturn()的次數)。

3) 被測函數調用被測類其他函數,怎麼控制返回值?

比如在CreateGameServiceImpl這個類中,有這樣一段函數

public class CreateGameServiceImpl implements CreateGameService {

    ...//省略成員變量

    public FullGame createGame(String name, Long creatorId, List<Point> points, List<Selection> selections, List<Line> lines) {
        Game gameItem = createBlankGame(name, creatorId);    //createBlankGame()爲CreateGameServiceImpl中另一個函數

那麼,如果我還沒實現createBlackGame(),我在測試函數裏應該怎麼控制它呢?這次用2)中的方法@Mock + @InjectMocks就不行了,因爲他們屬於同一個類。

(這個問題@Xander 覺得應該實現了被調用的函數纔好,但是既然mock的存在很多時候是爲了在函數都沒實現的情況下編寫測試,因此我覺得繼續研究。)

後來自己通過查閱**官方的文檔解決辦法**是使用spy()命令,結合doReturn()

public class CreateGameServiceImplTest {
    //這部分不需要改。省略其他成員變量
    @Mock
    private GameHelper gameHelper;

    @InjectMocks
    CreateGameServiceImpl serviceimpl;

    @Test
    public void testCreateGameStringLongListOfPointListOfSelectionListOfLine() {
        serviceimpl = spy(serviceimpl); //將serviceimpl部分mock化
        doReturn(***).when(serviceimpl).createBlankGame(a, b);  //這裏必須用doReturn()而不能是when().thenReturn()
        ...
    }
}

原因我們在最後解釋。

首先我們來看文檔中對於Spy()的解釋:

You can create spies of real objects. When you use the spy then the methods are called (unless a method was stubbed).

Spying on real objects can be associated with "partial mocking" concept.(重點是,spy與"部分mock"相關。)

對於Spy,官方有個Sample:

   List list = new LinkedList();
   List spy = spy(list);

   //optionally, you can stub out some methods:
   when(spy.size()).thenReturn(100);

   //using the spy calls real methods
   spy.add("one");
   spy.add("two");

   //prints "one" - 這個函數還是真實的
   System.out.println(spy.get(0));

   //100 is printed - size()函數被替換了
   System.out.println(spy.size());

通俗來講,在我個人理解,Spy()可以使一個對象的一部分方法被用戶替換

在我們的例子中,CreateGameServiceImpl中的函數createGame()調用了createBlankGame(),而後者可能是未實現的。

但是此時CreateGameServiceImpl類的註解是@InjectMocks而不是@Mock,只能接收@Mock對象的注入,而自己的方法無法被mock(stub)。

因此我們通過spy(),將CreateGameServiceImpl部分mock化,從而將createBlankGame()函數替換掉。

不過這裏如果遇到private的被調函數就沒辦法了。

覆蓋率

對於單元測試,一個重要的衡量指標就是覆蓋率

覆蓋率分爲:

行覆蓋(Line Coverage,又叫段覆蓋/語句覆蓋(Statement Coverage)等)

分支覆蓋(Branch Coverage,又叫判定覆蓋Decision Coverage等)

條件覆蓋(Condition Coverage)

路徑覆蓋(Path Coverage)等等

據瞭解,所有這些覆蓋中行覆蓋(Line coverage)是最簡單的,也是最常用的、最有效的覆蓋率。

在EclEmma中可以選擇任意一種覆蓋率,以下是我的項目中行覆蓋率的截圖。

3. 指令覆蓋(Instruction Coverage),方法覆蓋(Method Coverage):均爲100%,就不截圖了。

*額外工作:實現任意兩個對象比較

由於單元測試常常需要用到assertEquals()方法,而對於很多自定義的數據結構(比如Point.java)要重寫equals()方法,否則調用equals()只會比較兩個對象的引用,這帶來非常多的麻煩事。

鑑於Web應用中使用的自定義的數據結構(Model)通常是JavaBean規範的(這些類的成員屬性通常是Java的基本數據類型或String,Collection等常見類型),因此我希望通過只對比兩個對象的成員變量的變量類型、變量名、變量的值(如果是集合和數組就深入進去判斷),來判斷兩個對象是否“相等”

我查了好久,除了發現貌似還真沒人寫,只有apache.commons.beanutilsapache.commons.collections.comparator有類似的方法,但是看了他們的源碼覺得跟我要的不是一個東西。

這個工具類,因爲有些細節上寫起來很困難,大概寫了好幾個小時吧,打算以後如果真的沒人做這個,就把它做成一個開源的小工具。

在這裏我同時提供了兩種方式,第一種比較取巧,直接比較兩個對象對應的JSON字符串,這種方法很方便,不容易出錯,但是可能適用範圍上略小一點。

我使用的是JackJson的庫(其他也可以),代碼如下:

static public boolean compareByJson(Object a,Object b){
    try {
        ObjectMapper objectMapper = new ObjectMapper();
        String jsona =objectMapper.writeValueAsString(a);
        String jsonb =objectMapper.writeValueAsString(b);
        System.out.println(jsona);
        System.out.println(jsonb);
        return jsona.equals(jsonb);
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
}

第二種,則是通過Java的反射機制,通過getClass(),getDeclaredFields(),setAccessible(true)等方法來取得任意對象的成員變量,按順序分析兩者中的兩個變量的變量名、變量類型、變量值是否相等,是否是重寫了equals()方法的常見類型、是否是集合等方面來比較和判斷,也借用了LeetCode上一道難度很低的題的算法,代碼如下(某處仍有bug):

static public boolean compare(Object obj_a, Object obj_b) {
    if(obj_a == null && obj_b == null)
        return true;
    else if (obj_a == null || obj_b == null) 
        return false;
    else {
        Field[] fields_a = obj_a.getClass().getDeclaredFields();
        Field[] fields_b = obj_b.getClass().getDeclaredFields();
        if (fields_a.length != fields_b.length)
            return false;
        else for (int i = 0; i < fields_a.length; i++) {
            fields_a[i].setAccessible(true);
            fields_b[i].setAccessible(true);
            Object obj_a_innerobj_i = null, obj_b_innerobj_i = null;
            try {
                obj_a_innerobj_i = fields_a[i].get(obj_a);
                obj_b_innerobj_i = fields_b[i].get(obj_b);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            if (fields_a[i].getName() != fields_b[i].getName())
                return false;
            else if (!fields_a[i].getGenericType().equals(fields_b[i].getGenericType()))
                return false;
            else if (!SupportedClassesList.contains(obj_a_innerobj_i.getClass())) 
                if (compare(obj_a_innerobj_i, obj_b_innerobj_i) == false)
                    return false;
            else if (obj_a_innerobj_i instanceof Collection){
                //TODO 仍有bug
                if(!(((Collection) obj_a_innerobj_i).containsAll((Collection)(obj_b_innerobj_i))&&((Collection)obj_b_innerobj_i).containsAll((Collection)(obj_a_innerobj_i))))
                    return false;
                else if (!obj_a_innerobj_i.equals(obj_b_innerobj_i))
                    return false;
            }
        }
        return true;
}

相關鏈接:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章