使用強大的 Mockito 測試框架來測試你的代碼

使用強大的 Mockito 測試框架來測試你的代碼

這篇教程介紹瞭如何使用 Mockito 框架來給軟件寫測試用例

1. 預備知識

如果需要往下學習,你需要先理解 Junit 框架中的單元測試。

如果你不熟悉 JUnit,請查看下面的教程:
http://www.vogella.com/tutorials/JUnit/article.html

2. 使用mock對象來進行測試

2.1. 單元測試的目標和挑戰

單元測試的思路是在不涉及依賴關係的情況下測試代碼(隔離性),所以測試代碼與其他類或者系統的關係應該儘量被消除。一個可行的消除方法是替換掉依賴類(測試替換),也就是說我們可以使用替身來替換掉真正的依賴對象。

2.2. 測試類的分類

dummy object 做爲參數傳遞給方法但是絕對不會被使用。譬如說,這種測試類內部的方法不會被調用,或者是用來填充某個方法的參數。

Fake 是真正接口或抽象類的實現體,但給對象內部實現很簡單。譬如說,它存在內存中而不是真正的數據庫中。(譯者注:Fake 實現了真正的邏輯,但它的存在只是爲了測試,而不適合於用在產品中。)

stub 類是依賴類的部分方法實現,而這些方法在你測試類和接口的時候會被用到,也就是說 stub 類在測試中會被實例化。stub 類會迴應任何外部測試的調用。stub 類有時候還會記錄調用的一些信息。

mock object 是指類或者接口的模擬實現,你可以自定義這個對象中某個方法的輸出結果。

測試替代技術能夠在測試中模擬測試類以外對象。因此你可以驗證測試類是否響應正常。譬如說,你可以驗證在 Mock 對象的某一個方法是否被調用。這可以確保隔離了外部依賴的干擾只測試測試類。

我們選擇 Mock 對象的原因是因爲 Mock 對象只需要少量代碼的配置。

2.3. Mock 對象的產生

你可以手動創建一個 Mock 對象或者使用 Mock 框架來模擬這些類,Mock 框架允許你在運行時創建 Mock 對象並且定義它的行爲。

一個典型的例子是把 Mock 對象模擬成數據的提供者。在正式的生產環境中它會被實現用來連接數據源。但是我們在測試的時候 Mock 對象將會模擬成數據提供者來確保我們的測試環境始終是相同的。

Mock 對象可以被提供來進行測試。因此,我們測試的類應該避免任何外部數據的強依賴。

通過 Mock 對象或者 Mock 框架,我們可以測試代碼中期望的行爲。譬如說,驗證只有某個存在 Mock 對象的方法是否被調用了。

2.4. 使用 Mockito 生成 Mock 對象

Mockito 是一個流行 mock 框架,可以和JUnit結合起來使用。Mockito 允許你創建和配置 mock 對象。使用Mockito可以明顯的簡化對外部依賴的測試類的開發。

一般使用 Mockito 需要執行下面三步

  • 模擬並替換測試代碼中外部依賴。

  • 執行測試代碼

  • 驗證測試代碼是否被正確的執行

mockitousagevisualization

3. 爲自己的項目添加 Mockito 依賴

3.1. 在 Gradle 添加 Mockito 依賴

如果你的項目使用 Gradle 構建,將下面代碼加入 Gradle 的構建文件中爲自己項目添加 Mockito 依賴

repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.0.57-beta" }

3.2. 在 Maven 添加 Mockito 依賴

需要在 Maven 聲明依賴,您可以在 http://search.maven.org 網站中搜索 g:“org.mockito”, a:“mockito-core” 來得到具體的聲明方式。

3.3. 在 Eclipse IDE 使用 Mockito

Eclipse IDE 支持 Gradle 和 Maven 兩種構建工具,所以在 Eclipse IDE 添加依賴取決你使用的是哪一個構建工具。

3.4. 以 OSGi 或者 Eclipse 插件形式添加 Mockito 依賴

在 Eclipse RCP 應用依賴通常可以在 p2 update 上得到。Orbit 是一個很好的第三方倉庫,我們可以在裏面尋找能在 Eclipse 上使用的應用和插件。

Orbit 倉庫地址 http://download.eclipse.org/tools/orbit/downloads

orbit p2 mockito

4. 使用Mockito API

4.1. 靜態引用

如果在代碼中靜態引用了org.mockito.Mockito.*;,那你你就可以直接調用靜態方法和靜態變量而不用創建對象,譬如直接調用 mock() 方法。

4.2. 使用 Mockito 創建和配置 mock 對象

除了上面所說的使用 mock() 靜態方法外,Mockito 還支持通過 @Mock 註解的方式來創建 mock 對象。

如果你使用註解,那麼必須要實例化 mock 對象。Mockito 在遇到使用註解的字段的時候,會調用MockitoAnnotations.initMocks(this) 來初始化該 mock 對象。另外也可以通過使用@RunWith(MockitoJUnitRunner.class)來達到相同的效果。

通過下面的例子我們可以瞭解到使用@Mock 的方法和MockitoRule規則。

import static org.mockito.Mockito.*;

public class MockitoTest  {

        @Mock
        MyDatabase databaseMock; (1)

        @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); (2)

        @Test
        public void testQuery()  {
                ClassToTest t  = new ClassToTest(databaseMock); (3)
                boolean check = t.query("* from t"); (4)
                assertTrue(check); (5)
                verify(databaseMock).query("* from t"); (6)
        }
}
  1. 告訴 Mockito 模擬 databaseMock 實例

  2. Mockito 通過 @mock 註解創建 mock 對象

  3. 使用已經創建的mock初始化這個類

  4. 在測試環境下,執行測試類中的代碼

  5. 使用斷言確保調用的方法返回值爲 true

  6. 驗證 query 方法是否被 MyDatabase 的 mock 對象調用

4.3. 配置 mock

當我們需要配置某個方法的返回值的時候,Mockito 提供了鏈式的 API 供我們方便的調用

when(…​.).thenReturn(…​.)可以被用來定義當條件滿足時函數的返回值,如果你需要定義多個返回值,可以多次定義。當你多次調用函數的時候,Mockito 會根據你定義的先後順序來返回返回值。Mocks 還可以根據傳入參數的不同來定義不同的返回值。譬如說你的函數可以將anyString 或者 anyInt作爲輸入參數,然後定義其特定的放回值。

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

@Test
public void test1()  {
        //  創建 mock
        MyClass test = Mockito.mock(MyClass.class);

        // 自定義 getUniqueId() 的返回值
        when(test.getUniqueId()).thenReturn(43);

        // 在測試中使用mock對象
        assertEquals(test.getUniqueId(), 43);
}

// 返回多個值
@Test
public void testMoreThanOneReturnValue()  {
        Iterator i= mock(Iterator.class);
        when(i.next()).thenReturn("Mockito").thenReturn("rocks");
        String result=i.next()+" "+i.next();
        // 斷言
        assertEquals("Mockito rocks", result);
}

// 如何根據輸入來返回值
@Test
public void testReturnValueDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo("Mockito")).thenReturn(1);
        when(c.compareTo("Eclipse")).thenReturn(2);
        // 斷言
        assertEquals(1,c.compareTo("Mockito"));
}

// 如何讓返回值不依賴於輸入
@Test
public void testReturnValueInDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo(anyInt())).thenReturn(-1);
        // 斷言
        assertEquals(-1 ,c.compareTo(9));
}

// 根據參數類型來返回值
@Test
public void testReturnValueInDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo(isA(Todo.class))).thenReturn(0);
        // 斷言
        Todo todo = new Todo(5);
        assertEquals(todo ,c.compareTo(new Todo(1)));
}

對於無返回值的函數,我們可以使用doReturn(…​).when(…​).methodCall來獲得類似的效果。例如我們想在調用某些無返回值函數的時候拋出異常,那麼可以使用doThrow 方法。如下面代碼片段所示

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

// 下面測試用例描述瞭如何使用doThrow()方法

@Test(expected=IOException.class)
public void testForIOException() {
        // 創建並配置 mock 對象
        OutputStream mockStream = mock(OutputStream.class);
        doThrow(new IOException()).when(mockStream).close();

        // 使用 mock
        OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
        streamWriter.close();
}

4.4. 驗證 mock 對象方法是否被調用

Mockito 會跟蹤 mock 對象裏面所有的方法和變量。所以我們可以用來驗證函數在傳入特定參數的時候是否被調用。這種方式的測試稱行爲測試,行爲測試並不會檢查函數的返回值,而是檢查在傳入正確參數時候函數是否被調用。

import static org.mockito.Mockito.*;

@Test
public void testVerify()  {
        // 創建並配置 mock 對象
        MyClass test = Mockito.mock(MyClass.class);
        when(test.getUniqueId()).thenReturn(43);

        // 調用mock對象裏面的方法並傳入參數爲12
        test.testing(12);
        test.getUniqueId();
        test.getUniqueId();

        // 查看在傳入參數爲12的時候方法是否被調用
        verify(test).testing(Matchers.eq(12));

        // 方法是否被調用兩次
        verify(test, times(2)).getUniqueId();

        // 其他用來驗證函數是否被調用的方法
        verify(mock, never()).someMethod("never called");
        verify(mock, atLeastOnce()).someMethod("called at least once");
        verify(mock, atLeast(2)).someMethod("called at least twice");
        verify(mock, times(5)).someMethod("called five times");
        verify(mock, atMost(3)).someMethod("called at most 3 times");
}

4.5. 使用 Spy 封裝 java 對象

@Spy或者spy()方法可以被用來封裝 java 對象。被封裝後,除非特殊聲明(打樁 stub),否則都會真正的調用對象裏面的每一個方法

import static org.mockito.Mockito.*;

// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);

// 可用 doReturn() 來打樁
doReturn("foo").when(spy).get(0);

// 下面代碼不生效
// 真正的方法會被調用
// 將會拋出 IndexOutOfBoundsException 的異常,因爲 List 爲空
when(spy.get(0)).thenReturn("foo");

方法verifyNoMoreInteractions()允許你檢查沒有其他的方法被調用了。

4.6. 使用 @InjectMocks 在 Mockito 中進行依賴注入

我們也可以使用@InjectMocks 註解來創建對象,它會根據類型來注入對象裏面的成員方法和變量。假定我們有 ArticleManager 類

public class ArticleManager {
    private User user;
    private ArticleDatabase database;

    ArticleManager(User user) {
     this.user = user;
    }

    void setDatabase(ArticleDatabase database) { }
}

這個類會被 Mockito 構造,而類的成員方法和變量都會被 mock 對象所代替,正如下面的代碼片段所示:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest  {

       @Mock ArticleCalculator calculator;
       @Mock ArticleDatabase database;
       @Most User user;

       @Spy private UserProvider userProvider = new ConsumerUserProvider();

       @InjectMocks private ArticleManager manager; (1)

       @Test public void shouldDoSomething() {
               // 假定 ArticleManager 有一個叫 initialize() 的方法被調用了
               // 使用 ArticleListener 來調用 addListener 方法
               manager.initialize();

               // 驗證 addListener 方法被調用
               verify(database).addListener(any(ArticleListener.class));
       }
}
  1. 創建ArticleManager實例並注入Mock對象

更多的詳情可以查看
http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html.

4.7. 捕捉參數

ArgumentCaptor類允許我們在verification期間訪問方法的參數。得到方法的參數後我們可以使用它進行測試。

import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.Arrays;
import java.util.List;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class MockitoTests {
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Captor
    private ArgumentCaptor<List<String>> captor;

    @Test
    public final void shouldContainCertainListItem() {
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);

        verify(mockedList).addAll(captor.capture());
        final List<String> capturedArgument = captor.getValue();
        assertThat(capturedArgument, hasItem("someElement"));
    }
}

4.8. Mockito的限制

Mockito當然也有一定的限制。而下面三種數據類型則不能夠被測試

  • final classes

  • anonymous classes

  • primitive types

5. 在Android中使用Mockito

在 Android 中的 Gradle 構建文件中加入 Mockito 依賴後就可以直接使用 Mockito 了。若想使用 Android Instrumented tests 的話,還需要添加 dexmaker 和 dexmaker-mockito 依賴到 Gradle 的構建文件中。(需要 Mockito 1.9.5版本以上)

dependencies {
    testCompile 'junit:junit:4.12'
    // Mockito unit test 的依賴
    testCompile 'org.mockito:mockito-core:1.+'
    // Mockito Android instrumentation tests 的依賴
    androidTestCompile 'org.mockito:mockito-core:1.+'
    androidTestCompile "com.google.dexmaker:dexmaker:1.2"
    androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
}

6. 實例:使用Mockito寫一個Instrumented Unit Test

6.1. 創建一個測試的Android 應用

創建一個包名爲com.vogella.android.testing.mockito.contextmock的Android應用,添加一個靜態方法
,方法裏面創建一個包含參數的Intent,如下代碼所示:

public static Intent createQuery(Context context, String query, String value) {
    // 簡單起見,重用MainActivity
    Intent i = new Intent(context, MainActivity.class);
    i.putExtra("QUERY", query);
    i.putExtra("VALUE", value);
    return i;
}

6.2. 在app/build.gradle文件中添加Mockito依賴

dependencies {
    // Mockito 和 JUnit 的依賴
    // instrumentation unit tests on the JVM
    androidTestCompile 'junit:junit:4.12'
    androidTestCompile 'org.mockito:mockito-core:2.0.57-beta'
    androidTestCompile 'com.android.support.test:runner:0.3'
    androidTestCompile "com.google.dexmaker:dexmaker:1.2"
    androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

    // Mockito 和 JUnit 的依賴
    // tests on the JVM
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.+'

}

6.3. 創建測試

使用 Mockito 創建一個單元測試來驗證在傳遞正確 extra data 的情況下,intent 是否被觸發。

因此我們需要使用 Mockito 來 mock 一個Context對象,如下代碼所示:

package com.vogella.android.testing.mockitocontextmock;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class TextIntentCreation {

    @Test
    public void testIntentShouldBeCreated() {
        Context context = Mockito.mock(Context.class);
        Intent intent = MainActivity.createQuery(context, "query", "value");
        assertNotNull(intent);
        Bundle extras = intent.getExtras();
        assertNotNull(extras);
        assertEquals("query", extras.getString("QUERY"));
        assertEquals("value", extras.getString("VALUE"));
    }
}

7. 實例:使用 Mockito 創建一個 mock 對象

7.1. 目標

創建一個 Api,它可以被 Mockito 來模擬並做一些工作

7.2. 創建一個Twitter API 的例子

實現 TwitterClient類,它內部使用到了 ITweet 的實現。但是ITweet實例很難得到,譬如說他需要啓動一個很複雜的服務來得到。

public interface ITweet {

        String getMessage();
}


public class TwitterClient {

        public void sendTweet(ITweet tweet) {
                String message = tweet.getMessage();

                // send the message to Twitter
        }
}

7.3. 模擬 ITweet 的實例

爲了能夠不啓動複雜的服務來得到 ITweet,我們可以使用 Mockito 來模擬得到該實例。

@Test
public void testSendingTweet() {
        TwitterClient twitterClient = new TwitterClient();

        ITweet iTweet = mock(ITweet.class);

        when(iTweet.getMessage()).thenReturn("Using mockito is great");

        twitterClient.sendTweet(iTweet);
}

現在 TwitterClient 可以使用 ITweet 接口的實現,當調用 getMessage() 方法的時候將會打印 “Using Mockito is great” 信息。

7.4. 驗證方法調用

確保 getMessage() 方法至少調用一次。

@Test
public void testSendingTweet() {
        TwitterClient twitterClient = new TwitterClient();

        ITweet iTweet = mock(ITweet.class);

        when(iTweet.getMessage()).thenReturn("Using mockito is great");

        twitterClient.sendTweet(iTweet);

        verify(iTweet, atLeastOnce()).getMessage();
}

7.5. 驗證

運行測試,查看代碼是否測試通過。

8. 模擬靜態方法

8.1. 使用 Powermock 來模擬靜態方法

因爲 Mockito 不能夠 mock 靜態方法,因此我們可以使用 Powermock

import java.net.InetAddress;
import java.net.UnknownHostException;

public final class NetworkReader {
    public static String getLocalHostname() {
        String hostname = "";
        try {
            InetAddress addr = InetAddress.getLocalHost();
            // Get hostname
            hostname = addr.getHostName();
        } catch ( UnknownHostException e ) {
        }
        return hostname;
    }
}

我們模擬了 NetworkReader 的依賴,如下代碼所示:

import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;

@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {

// 測試代碼

 @Test
public void testSomething() {
    mockStatic( NetworkUtil.class );
    when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );

    // 與 NetworkReader 協作的測試
}

8.2.用封裝的方法代替Powermock

有時候我們可以在靜態方法周圍包含非靜態的方法來達到和 Powermock 同樣的效果。

class FooWraper { 
      void someMethod() { 
           Foo.someStaticMethod() 
       } 
}

9. Mockito 參考資料

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

https://github.com/mockito/mockito- Mockito Github

https://github.com/mockito/mockito/blob/master/doc/release-notes/official.md - Mockito 發行說明

http://martinfowler.com/articles/mocksArentStubs.html 與Mocks,Stub有關的文章

http://chiuki.github.io/advanced-android-espresso/ 高級android教程(竟然是個妹子)

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