Mock 測試

Mock 方法是單元測試中常見的一種技術,它的主要作用是模擬一些在應用中不容易構造或者比較復雜的對象,從而把測試與測試邊界以外的對象隔離開。

mock測試介紹

單元測試是保證程序正確性的一種有效的測試手段,對于不同的開發語言,通常都能找到相應的單元框架。


單元測試

框架幫我們提供了case的管理,執行,斷言集,運行參數,全局事件工作,所有的這些使得我們只需關注:于對于特定的輸入,被測對象的返回是否正常。
那么,這些xUnit系列的單元測試框架是如何做到這些的了?分析這些框架,發現所有的單元測試框架都是基于以下的一種體系結構設計的。

image.png

如上圖所示,單測框架中通常包括TestRunner, Test, TestResult, TestCase, TestSuite, TestFixture六個組件。
TestRuner:負責驅動單元測試用例的執行,匯報測試執行的結果,從而簡化測試
TestFixture:以測試套件的形式提供setUp()和tearDown()方法,保證兩個test case之間的執行是相互獨立,互不影響的。
TestResult:這個組件用于收集每個test case的執行結果
Test:作為TestSuite和TestCase的父類暴露run()方法為TestRunner調用
TestCase:暴露給用戶的一個類,用戶通過繼承TestCase,編寫自己的測試用例邏輯
TestSuite:提供suite功能管理testCase
正因為相似的體系結構,所以大多數單元測試框架都提供了類似的功能和使用方法。

單元測試框架的一些問題

我在單元測試框架中寫一個TestCase,與我單獨寫一個cpp文件在main()方法里寫測試代碼有什么本質卻別嗎?用了單元測試框架,并沒有解決我在對復雜系統做單測時遇到的問題。
沒錯,對于單個case這兩者從本質上說是沒有區別的。單元測試框架本身并沒有告訴你如何去寫TestCase,在這一點上他是沒有提供任何幫助的。所以對于一些復雜的場景,只用單元測試框架是有點多少顯得無能為力的。
使用單元測試框架往往適用于以下場景的測試:單個函數,一個class,或者幾個功能相關class的測試,對于純函數測試,接口級別的測試尤其適用,如房貸計算器公式的測試。
但是,對于一些復雜場景:
? 被測對象依賴復雜,甚至無法簡單new出這個對象
? 對于一些failure場景的測試
? 被測對象中涉及多線程合作
? 被測對象通過消息與外界交互的場景
? …
單純依賴單測框架是無法實現單元測試的,而從某種意義上來說,這些場景反而是測試中的重點。
以分布式系統的測試為例,class 與 function級別的單元測試對整個系統的幫助不大,當然,這種單元測試對單個程序的質量有幫助;分布式系統測試的要點是測試進程間的交互:一個進程收到客戶請求,該如何處理,然后轉發給其他進程;收到響應之后,又修改并應答客戶;同時分布式系統測試中通常更關注一些異常路徑的測試,這些場景才是測試中的重點,也是難點所在。
Mock方法的引入通常能幫助我們解決以上場景中遇到的難題。
Mock通常是指,在測試一個對象A時,我們構造一些假的對象來模擬與A之間的交互,而這些Mock對象的行為是我們事先設定且符合預期。通過這些Mock對象來測試A在正常邏輯,異常邏輯或壓力情況下工作是否正常。
引入Mock最大的優勢在于:Mock的行為固定,它確保當你訪問該Mock的某個方法時總是能夠獲得一個沒有任何邏輯的直接就返回的預期結果。
Mock Object的使用通常會帶來以下一些好處:

  • 隔絕其他模塊出錯引起本模塊的測試錯誤。
  • 隔絕其他模塊的開發狀態,只要定義好接口,不用管他們開發有沒有完成。
  • 一些速度較慢的操作,可以用Mock Object代替,快速返回。
    對于分布式系統的測試,使用Mock Object會有另外兩項很重要的收益:
  • 通過Mock Object可以將一些分布式測試轉化為本地的測試
  • 將Mock用于壓力測試,可以解決測試集群無法模擬線上集群大規模下的壓力

Mock的應用場景

在使用Mock的過程中,發現Mock是有一些通用性的,對于一些應用場景,是非常適合使用Mock的:

  • 真實對象具有不可確定的行為(產生不可預測的結果,如股票的行情)
  • 真實對象很難被創建(比如具體的web容器)
  • 真實對象的某些行為很難觸發(比如網絡錯誤)
  • 真實情況令程序的運行速度很慢- ? 真實對象有用戶界面
    ? 測試需要詢問真實對象它是如何被調用的(比如測試可能需要驗證某個回調函數是否被調用了)
    ? 真實對象實際上并不存在(當需要和其他開發小組,或者新的硬件系統打交道的時候,這是一個普遍的問題)
    當然,也有一些不得不Mock的場景:
    ? 一些比較難構造的Object:這類Object通常有很多依賴,在單元測試中構造出這樣類通常花費的成本太大。
    ? 執行操作的時間較長Object:有一些Object的操作費時,而被測對象依賴于這一個操作的執行結果,例如大文件寫操作,數據的更新等等,出于測試的需求,通常將這類操作進行Mock。
    ? 異常邏輯:一些異常的邏輯往往在正常測試中是很難觸發的,通過Mock可以人為的控制觸發異常邏輯。
    在一些壓力測試的場景下,也不得不使用Mock,例如在分布式系統測試中,通常需要測試一些單點(如namenode,jobtracker)在壓力場景下的工作是否正常。而通常測試集群在正常邏輯下無法提供足夠的壓力(主要原因是受限于機器數量),這時候就需要應用Mock去滿足。

5 Mock工具的介紹

手動的構造 Mock 對象通常帶來額外的編碼量,而且這些為創建 Mock 對象而編寫的代碼很有可能引入錯誤。目前,有許多開源項目對動態構建 Mock 對象提供了支持,這些項目能夠根據現有的接口或類動態生成,這樣不僅能避免額外的編碼工作,同時也降低了引入錯誤的可能。
C++: GoogleMock http://code.google.com/p/googlemock/
Java: EasyMock http://easymock.org/
通常Mock工具通過簡單的方法對于給定的接口生成 Mock 對象的類庫。它提供對接口的模擬,能夠通過錄制、回放、檢查三步來完成大體的測試過程,可以驗證方法的調用種類、次數、順序,可以令 Mock 對象返回指定的值或拋出指定異常。通過這些Mock工具我們可以方便的構造 Mock 對象從而使單元測試順利進行,能夠應用于更加復雜的測試場景。以EasyMock為例,通過 EasyMock,我們可以為指定的接口動態的創建 Mock 對象,并利用 Mock 對象來模擬協同模塊,從而使單元測試順利進行。這個過程大致可以劃分為以下幾個步驟:
? 使用 EasyMock 生成 Mock 對象
? 設定 Mock 對象的預期行為和輸出
? 將 Mock 對象切換到 Replay 狀態
? 調用 Mock 對象方法進行單元測試
? 對 Mock 對象的行為進行驗證

使用 EasyMock 生成 Mock 對象

根據指定的接口或類,EasyMock 能夠動態的創建 Mock 對象(EasyMock 默認只支持為接口生成 Mock 對象,如果需要為類生成 Mock 對象,在 EasyMock 的主頁上有擴展包可以實現此功能),我們以 ResultSet 接口為例說明EasyMock的功能。java.sql.ResultSet 是每一個 Java 開發人員都非常熟悉的接口:
ResultSet 接口
public interface java.sql.ResultSet {
......
public abstract java.lang.String getString(int arg0) throws java.sql.SQLException;
public abstract double getDouble(int arg0) throws java.sql.SQLException;
......
}
通常,構建一個真實的 RecordSet 對象需要經過一個復雜的過程:在開發過程中,開發人員通常會編寫一個 DBUtility 類來獲取數據庫連接 Connection,并利用 Connection 創建一個 Statement。執行一個 Statement 可以獲取到一個或多個 ResultSet 對象。這樣的構造過程復雜并且依賴于數據庫的正確運行。數據庫或是數據庫交互模塊出現問題,都會影響單元測試的結果。
我們可以使用 EasyMock 動態構建 ResultSet 接口的 Mock 對象來解決這個問題。一些簡單的測試用例只需要一個 Mock 對象,這時,我們可以用以下的方法來創建 Mock 對象:
ResultSet mockResultSet =easyMock.createMock(ResultSet.class);

如果需要在相對復雜的測試用例中使用多個 Mock 對象,EasyMock 提供了另外一種生成和管理 Mock 對象的機制:
IMocksControl control = EasyMock.createControl();
java.sql.Connection mockConnection = control.createMock(Connection.class);
java.sql.Statement mockStatement = control.createMock(Statement.class);
java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class);

如果要模擬的是一個具體類而非接口,那么您需要下載擴展包 EasyMock Class Extension 2.2.2。在對具體類進行模擬時,您只要用 org.easymock.classextension.EasyMock 類中的靜態方法代替 org.easymock.EasyMock 類中的靜態方法即可。

設定 Mock 對象的預期行為和輸出

在一個完整的測試過程中,一個 Mock 對象將會經歷兩個狀態:Record 狀態和 Replay 狀態。Mock 對象一經創建,它的狀態就被置為 Record。在 Record 狀態,用戶可以設定 Mock 對象的預期行為和輸出,這些對象行為被錄制下來,保存在 Mock 對象中。
添加 Mock 對象行為的過程通常可以分為以下3步:

  • 對 Mock 對象的特定方法作出調用
  • 通過 org.easymock.EasyMock 提供的靜態方法 expectLastCall 獲取上一次方法調用所對應的 IExpectationSetters 實例;
  • 通過 IExpectationSetters 實例設定 Mock 對象的預期輸出。

設定預期返回值

Mock 對象的行為可以簡單的理解為 Mock 對象方法的調用和方法調用所產生的輸出。
在 EasyMock 2.3 中,對 Mock 對象行為的添加和設置是通過接口 IExpectationSetters來實現的。
Mock 對象方法的調用可能產生兩種類型的輸出:
(1)產生返回值;
(2)拋出異常。接口 IExpectationSetters 提供了多種設定預期輸出的方法,其中和設定返回值相對應的是 andReturn 方法.
IExpectationSetters<T> andReturn(T value);
我們仍然用 ResultSet 接口的 Mock 對象為例,如果希望方法 mockResult.getString(1) 的返回值為 "My return value",那么你可以使用以下的語句:
mockResultSet.getString(1);
expectLastCall().andReturn("My return value");
有時,我們希望某個方法的調用總是返回一個相同的值,為了避免每次調用都為 Mock 對象的行為進行一次設定,我們可以用設置默認返回值的方法:
void andStubReturn(Object value);

舉例:
假設我們創建了 Statement 和 ResultSet 接口的 Mock 對象 mockStatement 和 mockResultSet,在測試過程中,我們希望 mockStatement 對象的 executeQuery 方法總是返回 mockResultSet,我們可以使用如下的語句
mockStatement.executeQuery("SELECT * FROM sales_order_table");
expectLastCall().andStubReturn(mockResultSet);

EasyMock 在對參數值進行匹配時,默認采用 Object.equals() 方法。因此,如果我們以 "select * from sales_order_table" 作為參數,預期方法將不會被調用。如果您希望上例中的 SQL 語句能不區分大小寫,可以用特殊的參數匹配器來解決這個問題,我們將在 "在 EasyMock 中使用參數匹配器" 一章對此進行說明。

設定預期異常拋出

對象行為的預期輸出除了可能是返回值外,還有可能是拋出異常。IExpectationSetters 提供了設定預期拋出異常的方法:
IExpectationSetters<T> andThrow(Throwable throwable);
和設定默認返回值類似,IExpectationSetters 接口也提供了設定拋出默認異常的函數:
void andStubThrow(Throwable throwable);

設定預期方法調用次數

通過以上的函數,您可以對 Mock 對象特定行為的預期輸出進行設定。除了對預期輸出進行設定,IExpectationSetters 接口還允許用戶對方法的調用次數作出限制。在 IExpectationSetters 所提供的這一類方法中,常用的一種是 times 方法:
IExpectationSetters<T>times(int count);
該方法可以 Mock 對象方法的調用次數進行確切的設定。

舉例:
假設我們希望 mockResultSet 的 getString 方法在測試過程中被調用3次,期間的返回值都是 "My return value",我們可以用如下語句:
mockResultSet.getString(1);
expectLastCall().andReturn("My return value").times(3);

除了設定確定的調用次數,IExpectationSetters 還提供了另外幾種設定非準確調用次數的方法:
times(int minTimes, int maxTimes):該方法最少被調用 minTimes 次,最多被調用 maxTimes 次。
atLeastOnce():該方法至少被調用一次。
anyTimes():該方法可以被調用任意次。
某些方法的返回值類型是 void,對于這一類方法,我們無需設定返回值,只要設置調用次數就可以了。以 ResultSet 接口的 close 方法為例,假設在測試過程中,該方法被調用3至5次:
mockResultSet.close();
expectLastCall().times(3, 5);

將 Mock 對象切換到 Replay 狀態

在生成 Mock 對象和設定 Mock 對象行為兩個階段,Mock 對象的狀態都是 Record 。在這個階段,Mock 對象會記錄用戶對預期行為和輸出的設定。
在使用 Mock 對象進行實際的測試前,我們需要將 Mock 對象的狀態切換為 Replay。在 Replay 狀態,Mock 對象能夠根據設定對特定的方法調用作出預期的響應。將 Mock 對象切換成 Replay 狀態有兩種方式,您需要根據 Mock 對象的生成方式進行選擇。如果 Mock 對象是通過 org.easymock.EasyMock 類提供的靜態方法 createMock 生成的(第1節中介紹的第一種 Mock 對象生成方法),那么 EasyMock 類提供了相應的 replay 方法用于將 Mock 對象切換為 Replay 狀態:
**replay(mockResultSet);**
如果 Mock 對象是通過 IMocksControl 接口提供的 createMock 方法生成的(第1節中介紹的第二種Mock對象生成方法),那么您依舊可以通過 IMocksControl 接口對它所創建的所有 Mock 對象進行切換:
control.replay();
以上的語句能將在第1節中生成的 mockConnection、mockStatement 和 mockResultSet 等3個 Mock 對象都切換成 Replay 狀態。

Mock 對象的重用

為了避免生成過多的 Mock 對象,EasyMock 允許對原有 Mock 對象進行重用。要對 Mock 對象重新初始化,我們可以采用 reset 方法。
和 replay 和 verify 方法類似,EasyMock 提供了兩種 reset 方式:
(1)如果 Mock 對象是由 org.easymock.EasyMock 類中的靜態方法 createMock 生成的,那么該 Mock 對象的可以用 EasyMock 類的靜態方法 reset 重新初始化;
(2)如果 Mock 方法是由 IMocksControl 實例的 createMock 方法生成的,那么該 IMocksControl 實例方法 reset 的調用將會把所有該實例創建的 Mock 對象重新初始化。
在重新初始化之后,Mock 對象的狀態將被置為 Record 狀態。

測試demo

  • 測試一個接口
public class UserServiceImpl{  
    private UserDao dao;  
    public User query(String id) throws Exception{  
        try{  
    return dao.getById(id);  
}catch(Exception e){  
    throw e;  
  }  
  return null;  
  }  
}  
  
public class UserDao{  
    public User getById(String id) throws Exception{  
        try{  
      return ……;  
    }catch(Exception e){  
    throw e;  
  }  
    return null;  
  }  
} 

測試代碼

public class UserServiceImplTest {  
        @Test  
        public void testQuery() {  
            User expectedUser = new User();  
            user.setId(“1001”);  
            UserDao mock  = EasyMock.createMock(UserDao.class);//創建Mock對象  
           Easymock.expect(mock.getById("1001")).andReturn(expectedUser);//錄制Mock對象預期行為  
            Easymock.replay(mock);//重放Mock對象,測試時以錄制的對象預期行為代替真實對象的行為  
  
            UserServiceImpl  service = new UserServiceImpl();  
            service.setUserDao(mock);  
            user user = service.query("1001");//調用測試方法  
            assertEquals(expectedUser, user); //斷言測試結果   
            Easymock.verify(mock);//驗證Mock對象被調用  
        }  
    }   
  • 測試一個對象
public class SalesOrderTestCase extends TestCase {
  public void testSalesOrder() {
    IMocksControl control = EasyMock.createControl();
    ......
    ResultSet mockResultSet = control.createMock(ResultSet.class);
    try {
      ......
      mockResultSet.next();
      expectLastCall().andReturn(true).times(3);
      expectLastCall().andReturn(false).times(1);
      mockResultSet.getString(1);
      expectLastCall().andReturn("DEMO_ORDER_001").times(1);
      expectLastCall().andReturn("DEMO_ORDER_002").times(1);
      expectLastCall().andReturn("DEMO_ORDER_003").times(1);
      mockResultSet.getString(2);
      expectLastCall().andReturn("Asia Pacific").times(1);
      expectLastCall().andReturn("Europe").times(1);
      expectLastCall().andReturn("America").times(1);
      mockResultSet.getDouble(3);
      expectLastCall().andReturn(350.0).times(1);
      expectLastCall().andReturn(1350.0).times(1);
      expectLastCall().andReturn(5350.0).times(1);
      control.replay();
      ......
      int i = 0;
      String[] priceLevels = { "Level_A", "Level_C", "Level_E" };
      while (mockResultSet.next()) {
        SalesOrder order = new SalesOrderImpl();
        order.loadDataFromDB(mockResultSet);
        assertEquals(order.getPriceLevel(), priceLevels[i]);
        i++;
      }
      control.verify();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容