模擬測試框架-Mockito

Coding-lover發表於2016-01-30

介紹

本文將介紹模擬測試框架Mockito的一些基礎概念, 介紹該框架的優點,講解應用Mockito的Java示例.

模擬(Mock)的概念

在軟體開發的世界之外, “mock”一詞是指模仿或者效仿. 因此可以將“mock”理解為一個替身,替代者. 在軟體開發中提及”mock”,通常理解為模擬物件或者Fake.
譯者注:mock等多代表的是對被模擬物件的抽象類,你可以把fake理解為mock的例項。不知道這樣說準不準確:)

Fake通常被用作被測類的依賴關係的替代者.

名次定義
依賴關係 – 依賴關係是指在應用程式中一個類基於另一個類來執行其預定的功能.依賴關係通常都存在於所依賴的類的例項變數中.

被測類 – 在編寫單元測試的時候, “單元”一詞通常代表一個單獨的類及為其編寫的測試程式碼. 被測類指的就是其中被測試的類.

為什麼需要模擬?
在我們一開始學程式設計時,我們所寫的物件通常都是獨立的. hello world之類的類並不依賴其他的類(System.out除外),也不會操作別的類.但實際上,軟體中是充滿依賴關係的.我們會基於service類寫操作類,而service類又是基於資料訪問類(DAOs)的,依次下去.

單元測試的思路就是我們想在不涉及依賴關係的情況下測試程式碼. 這種測試可以讓你無視程式碼的依賴關係去測試程式碼的有效性.核心思想就是如果程式碼按設計正常工作,並且依賴關係也正常,那麼他們應該會同時工作正常.
下面的程式碼就是這樣的例子:

import java.util.ArrayList;
public class Counter {
     public Counter() {
     }
     public int count(ArrayList items) {
          int results = 0;
          for(Object curItem : items) {
               results ++;
          }
          return results;
     }
}

如你所見,上面的例子十分簡單,但它闡明瞭要點.當你想要測試count方法時,你會針對count方法本身如何工作去寫測試程式碼. 你不會去測試ArrayList是否正常工作,因為你預設它已經被測過並且工作正常.你唯一的目標就是測試對ArrayList的使用.

模擬有哪些關鍵點?

在談到模擬時,你只需關心三樣東西: 設定測試資料,設定預期結果,驗證結果.一些單元測試方案根本就不涉及這些,有的只涉及設定測試資料,有的只涉及設定預期結果和驗證.

Stubbing (樁)

Stubbing就是告訴fake當與之互動時執行何種行為過程。通常它可以用來提供那些測試所需的公共屬性(像getters和setters)和公共方法。
當談到stubbing方法,通常你有一系列的選擇。或許你希望返回一個特殊的值,丟擲一個錯誤或者觸發一個事件,此外,你可能希望指出方法被呼叫時的不同行為(即通過傳遞匹配的型別或者引數給方法)。

這咋一聽起來工作量很大,但通常並非這樣。許多mocking框架的一個重要功能就是你不需要提供stub 的實體方法,也不用在執行測試期間stub那些未被呼叫的方法或者未使用的屬性。

設定預期

Fake的一個關鍵的特性就是當你用它進行模擬測試時你能夠告訴它你預期的結果。例如,你可以要求一個特定的函式被準確的呼叫3次,或不被呼叫,或呼叫至少兩次但不超過5次,或者需要滿足特定型別的引數、特定值和以上任意的組合的呼叫。可能性是無窮的。

通過設定預期結果告訴fake你期望發生的事情。因為它是一個模擬測試,所以實際上什麼也沒發生。但是,對於被測試的類來說,它並無法區分這種情況。所以fake能夠呼叫函式並讓它做它該做的。
值得注意的是,大多數模擬框架除了可以建立介面的模擬測試外,還可以建立公有類的模擬測試。

驗證預期結果

設定預期和驗證預期是同時進行的。設定預期在呼叫測試類的函式之前完成,驗證預期則在它之後。所以,首先你設定好預期結果,然後去驗證你的預期結果是否正確。
在一個單元測試中,如果你設定的預期沒有得到滿足,那麼這個單元測試就是失敗了。例如,你設定預期結果是 ILoginService.login函式必須用特定的使用者名稱和密碼被呼叫一次,但是在測試中它並沒有被呼叫,這個fake沒被驗證,所以測試失敗。

模擬的好處是什麼?

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

這是個最大的好處吧。如果你建立了一個Mock那麼你就可以在service介面建立之前寫Service Tests了,這樣你就能在開發過程中把測試新增到你的自動化測試環境中了。換句話說,模擬使你能夠使用測試驅動開發。

團隊可以並行工作

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

你可以建立一個驗證或者演示程式。

由於Mocks非常高效,Mocks可以用來建立一個概念證明,作為一個示意圖,或者作為一個你正考慮構建專案的演示程式。這為你決定專案接下來是否要進行提供了有力的基礎,但最重要的還是提供了實際的設計決策。

為無法訪問的資源編寫測試

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

Mock 可以交給使用者

在有些情況下,某種原因你需要允許一些外部來源訪問你的測試系統,像合作伙伴或者客戶。這些原因導致別人也可以訪問你的敏感資訊,而你或許只是想允許訪問部分測試環境。在這種情況下,如何向合作伙伴或者客戶提供一個測試系統來開發或者做測試呢?最簡單的就是提供一個mock,無論是來自於你的網路或者客戶的網路。soapUI mock非常容易配置,他可以執行在soapUI或者作為一個war包釋出到你的java伺服器裡面。

隔離系統

有時,你希望在沒有系統其他部分的影響下測試系統單獨的一部分。由於其他系統部分會給測試資料造成干擾,影響根據資料收集得到的測試結論。使用mock你可以移除掉除了需要測試部分的系統依賴的模擬。當隔離這些mocks後,mocks就變得非常簡單可靠,快速可預見。這為你提供了一個移除了隨機行為,有重複模式並且可以監控特殊系統的測試環境。

Mockito 框架

Mockito 是一個基於MIT協議的開源java測試框架。
Mockito區別於其他模擬框架的地方主要是允許開發者在沒有建立“預期”時驗證被測系統的行為。對於mock物件的一個評價是測試系統的測試程式碼是一個高耦合的,由於 Mockito試圖通過移除“期望規範”去除expect-run-verify(期望驗證模式)的模式,因此在耦合度上有所降低。這樣的結果是簡化了測試程式碼,使他更易讀和修改了。

你可以驗證互動:

// 模擬建立
List mockedList = mock(List.class);
// 使用模擬物件
mockedList.add("one");
mockedList.clear();
// 驗證選擇性和顯式呼叫
verify(mockedList).add("one");
verify(mockedList).clear();

設定方法呼叫資料

// 你不僅可以模擬介面,任何具體類都行
LinkedList mockedList = mock(LinkedList.class);
// 執行前準備測試資料
when(mockedList.get(0)).thenReturn("first");
// 接著列印"first"
System.out.println(mockedList.get(0));
// 因為get(999)未對準備資料,所以下面將列印"null".
System.out.println(mockedList.get(999));

使用Mockito框架的簡單Java程式碼示例

使用Mock框架

使用Mockito框架

步驟 1: 在Eclipse中建立一個Maven專案
定義thepom.xmlas檔案如下:

<?xml version="1.0" encoding="UTF-8"?>
<pre><project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>vn.com.phatbeo.ut.mockito.demo</groupId>
  <artifactId>demoMockito</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>demoMockito</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <testSourceDirectory>test</testSourceDirectory>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.1</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      </plugins>
  </build>

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

</project>

步驟 2: 新增java原始碼
ClassPerson.java

package vn.com.enclave.phatbeo.ut.mockito.demo;
/**
 * @author Phat (Phillip) H. VU <vuhongphat@hotmail.com>
 * 
 */
public class Person
{
    private final Integer personID;
    private final String personName;
    public Person( Integer personID, String personName )
    {
        this.personID = personID;
        this.personName = personName;
    }
    public Integer getPersonID()
    {
        return personID;
    }
    public String getPersonName()
    {
        return personName;
    }
}

介面類 PersonDAO.java

package vn.com.enclave.phatbeo.ut.mockito.demo;
/**
 * @author Phat (Phillip) H. VU <vuhongphat@hotmail.com>
 *
 */
public interface PersonDao
{
    public Person fetchPerson( Integer personID );
    public void update( Person person );
}

類 PersonService.java

package vn.com.enclave.phatbeo.ut.mockito.demo;
/**
 * @author Phat (Phillip) H. VU <vuhongphat@hotmail.com>
 *
 */
public class PersonService
{
    private final PersonDao personDao;
    public PersonService( PersonDao personDao )
    {
        this.personDao = personDao;
    }
    public boolean update( Integer personId, String name )
    {
        Person person = personDao.fetchPerson( personId );
        if( person != null )
        {
            Person updatedPerson = new Person( person.getPersonID(), name );
            personDao.update( updatedPerson );
            return true;
        }
        else
        {
            return false;
        }
    }
}

步驟 3: 新增單元測試類.
接下來為classPersonService.java編寫單元測試用例
可以設計classPersionServiceTest.java為如下:

package vn.com.enclave.phatbeo.ut.mockito.demo.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
 * @author Phat (Phillip) H. VU <vuhongphat@hotmail.com>
 *
 */
public class PersonServiceTest
{
    @Mock
    private PersonDao personDAO;
    private PersonService personService;
    @Before
    public void setUp()
        throws Exception
    {
        MockitoAnnotations.initMocks( this );
        personService = new PersonService( personDAO );
    }
    @Test
    public void shouldUpdatePersonName()
    {
        Person person = new Person( 1, "Phillip" );
        when( personDAO.fetchPerson( 1 ) ).thenReturn( person );
        boolean updated = personService.update( 1, "David" );
        assertTrue( updated );
        verify( personDAO ).fetchPerson( 1 );
        ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass( Person.class );
        verify( personDAO ).update( personCaptor.capture() );
        Person updatedPerson = personCaptor.getValue();
        assertEquals( "David", updatedPerson.getPersonName() );
        // asserts that during the test, there are no other calls to the mock object.
        verifyNoMoreInteractions( personDAO );
    }
    @Test
    public void shouldNotUpdateIfPersonNotFound()
    {
        when( personDAO.fetchPerson( 1 ) ).thenReturn( null );
        boolean updated = personService.update( 1, "David" );
        assertFalse( updated );
        verify( personDAO ).fetchPerson( 1 );
        verifyZeroInteractions( personDAO );
        verifyNoMoreInteractions( personDAO );
    }
}

關注點

  • Mock框架是什麼.
  • 為什麼要在測試中使用Mockito.

參考

http://java.dzone.com/articles/the-concept-mocking
http://en.wikipedia.org/wiki/Mockito
http://code.google.com/p/mockito
http://liuzhijun.iteye.com/blog/1512780/

相關文章