1. 程式人生 > >EasyMock 使用方法與原理剖析

EasyMock 使用方法與原理剖析

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

編寫自定義的 Mock 物件需要額外的編碼工作,同時也可能引入錯誤。EasyMock 提供了根據指定介面動態構建 Mock 物件的方法,避免了手工編寫 Mock 物件。本文將向您展示如何使用 EasyMock 進行單元測試,並對 EasyMock 的原理進行分析。

1.Mock 物件與 EasyMock 簡介

單元測試與 Mock 方法

單元測試是對應用中的某一個模組的功能進行驗證。在單元測試中,我們常遇到的問題是應用中其它的協同模組尚未開發完成,或者被測試模組需要和一些不容易構造、比較複雜的物件進行互動。另外,由於不能肯定其它模組的正確性,我們也無法確定測試中發現的問題是由哪個模組引起的。

Mock 物件能夠模擬其它協同模組的行為,被測試模組通過與 Mock 物件協作,可以獲得一個孤立的測試環境。此外,使用 Mock 物件還可以模擬在應用中不容易構造(如 HttpServletRequest 必須在 Servlet 容器中才能構造出來)和比較複雜的物件(如 JDBC 中的 ResultSet 物件),從而使測試順利進行。

EasyMock 簡介

手動的構造 Mock 物件會給開發人員帶來額外的編碼量,而且這些為建立 Mock 物件而編寫的程式碼很有可能引入錯誤。目前,有許多開源專案對動態構建 Mock 物件提供了支援,這些專案能夠根據現有的介面或類動態生成,這樣不僅能避免額外的編碼工作,同時也降低了引入錯誤的可能。

EasyMock 是一套用於通過簡單的方法對於給定的介面生成 Mock 物件的類庫。它提供對介面的模擬,能夠通過錄制、回放、檢查三步來完成大體的測試過程,可以驗證方法的呼叫種類、次數、順序,可以令 Mock 物件返回指定的值或丟擲指定異常。通過 EasyMock,我們可以方便的構造 Mock 物件從而使單元測試順利進行。

安裝 EasyMock

EasyMock 是採用 MIT license 的一個開源專案,您可以在 Sourceforge 上下載到相關的 zip 檔案。目前您可以下載的 EasyMock 最新版本是2.3,它需要執行在 Java 5.0 平臺上。如果您的應用執行在 Java 1.3 或 1.4 平臺上,您可以選擇 EasyMock1.2。在解壓縮 zip 包後,您可以找到 easymock.jar 這個檔案。如果您使用 Eclipse 作為 IDE,把 easymock.jar 新增到專案的 Libraries 裡就可以使用了(如下圖所示)。此外,由於我們的測試用例執行在 JUnit 環境中,因此您還需要 JUnit.jar(版本3.8.1以上)。

圖1:Eclipse 專案中的 Libraries
Eclipse 專案中的 Libraries

2.使用 EasyMock 進行單元測試

通過 EasyMock,我們可以為指定的介面動態的建立 Mock 物件,並利用 Mock 物件來模擬協同模組或是領域物件,從而使單元測試順利進行。這個過程大致可以劃分為以下幾個步驟:

  • 使用 EasyMock 生成 Mock 物件;
  • 設定 Mock 物件的預期行為和輸出;
  • 將 Mock 物件切換到 Replay 狀態;
  • 呼叫 Mock 物件方法進行單元測試;
  • 對 Mock 物件的行為進行驗證。

接下來,我們將對以上的幾個步驟逐一進行說明。除了以上的基本步驟外,EasyMock 還對特殊的 Mock 物件型別、特定的引數匹配方式等功能提供了支援,我們將在之後的章節中進行說明。

使用 EasyMock 生成 Mock 物件

根據指定的介面或類,EasyMock 能夠動態的建立 Mock 物件(EasyMock 預設只支援為介面生成 Mock 物件,如果需要為類生成 Mock 物件,在 EasyMock 的主頁上有擴充套件包可以實現此功能),我們以 ResultSet 介面為例說明EasyMock的功能。java.sql.ResultSet 是每一個 Java 開發人員都非常熟悉的介面:

清單1: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 = createMock(ResultSet.class);

其中 createMock 是 org.easymock.EasyMock 類所提供的靜態方法,你可以通過 static import 將其引入(注:static import 是 java 5.0 所提供的新特性)。

如果需要在相對複雜的測試用例中使用多個 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 類的 createControl 方法能建立一個介面 IMocksControl 的物件,該物件能建立並管理多個 Mock 物件。如果需要在測試中使用多個 Mock 物件,我們推薦您使用這一機制,因為它在多個 Mock 物件的管理上提供了相對便捷的方法。

如果您要模擬的是一個具體類而非介面,那麼您需要下載擴充套件包 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");

以上的語句表示 mockResultSet 的 getString 方法被呼叫一次,這次呼叫的返回值是 "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);


注意到 andReturn 和 andThrow 方法的返回值依然是一個 IExpectationSetters 例項,因此我們可以在此基礎上繼續呼叫 times 方法。

除了設定確定的呼叫次數,IExpectationSetters 還提供了另外幾種設定非準確呼叫次數的方法:
times(int minTimes, int maxTimes):該方法最少被呼叫 minTimes 次,最多被呼叫 maxTimes 次。
atLeastOnce():該方法至少被呼叫一次。
anyTimes():該方法可以被呼叫任意次。

某些方法的返回值型別是 void,對於這一類方法,我們無需設定返回值,只要設定呼叫次數就可以了。以 ResultSet 介面的 close 方法為例,假設在測試過程中,該方法被呼叫3至5次:

mockResultSet.close();
expectLastCall().times(3, 5);

為了簡化書寫,EasyMock 還提供了另一種設定 Mock 物件行為的語句模式。對於上例,您還可以將它寫成:

expect(mockResult.close()).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 物件方法進行單元測試

為了更好的說明 EasyMock 的功能,我們引入 src.zip 中的示例來解釋 Mock 物件在實際測試階段的作用。其中所有的示例程式碼都可以在 src.zip 中找到。如果您使用的 IDE 是 Eclipse,在匯入 src.zip 之後您可以看到 Workspace 中增加的 project(如下圖所示)。

圖2:匯入 src.zip 後的 Workspace
匯入src.zip後的Workspace

下面是示例程式碼中的一個介面 SalesOrder,它的實現類 SalesOrderImpl 的主要功能是從資料庫中讀取一個 Sales Order 的 Region 和 Total Price,並根據讀取的資料計算該 Sales Order 的 Price Level(完整的實現程式碼都可以在 src.zip 中找到):

清單2:SalesOrder 介面
public interface SalesOrder
{
  ……
  public void loadDataFromDB(ResultSet resultSet) throws SQLException;	
  public String getPriceLevel();
}

其實現類 SalesOrderImpl 中對 loadDataFromDB 的實現如下:

清單3:SalesOrderImpl 實現
public class SalesOrderImpl implements SalesOrder
{
  ......
  public void loadDataFromDB(ResultSet resultSet) throws SQLException
  {
    orderNumber = resultSet.getString(1);
    region = resultSet.getString(2);
    totalPrice = resultSet.getDouble(3);
  }
  ......
}

方法 loadDataFromDB 讀取了 ResultSet 物件包含的資料。當我們將之前定義的 Mock 物件調整為 Replay 狀態,並將該物件作為引數傳入,那麼 Mock 物件的方法將會返回預先定義的預期返回值。完整的 TestCase 如下:

清單4:完整的TestCase
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();
    }
  }
}

在這個示例中,我們首先建立了 ResultSet 的 Mock 物件 moResultSet,並記錄該 Mock 物件的預期行為。之後我們呼叫了 control.replay(),將 Mock 物件的狀態置為 Replay 狀態。 在實際的測試階段,Sales Order 物件的 loadDataFromDB 方法呼叫了 mockResultSet 物件的 getString 和 getDouble 方法讀取 mockResultSet 中的資料。Sales Order 物件根據讀取的資料計算出 Price Level,並和預期輸出進行比較。

對 Mock 物件的行為進行驗證

在利用 Mock 物件進行實際的測試過程之後,我們還有一件事情沒有做:對 Mock 物件的方法呼叫的次數進行驗證。

為了驗證指定的方法呼叫真的完成了,我們需要呼叫 verify 方法進行驗證。和 replay 方法類似,您需要根據 Mock 物件的生成方式來選用不同的驗證方式。如果 Mock 物件是由 org.easymock.EasyMock 類提供的 createMock 靜態方法生成的,那麼我們同樣採用 EasyMock 類的靜態方法 verify 進行驗證:

verify(mockResultSet);

如果Mock物件是有 IMocksControl 介面所提供的 createMock 方法生成的,那麼採用該介面提供的 verify 方法,例如第1節中的 IMocksControl 例項 control:

control.verify();

將對 control 例項所生成的 Mock 物件 mockConnection、mockStatement 和 mockResultSet 等進行驗證。如果將上例中 expectLastCall().andReturn(false).times(1) 的預期次數修改為2,在 Eclipse 中將可以看到:

圖3:Mock物件驗證失敗
Mock物件驗證失敗

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 物件重新初始化。

相關推薦

EasyMock 使用方法原理剖析

Mock 方法是單元測試中常見的一種技術,它的主要作用是模擬一些在應用中不容易構造或者比較複雜的物件,從而把測試與測試邊界以外的物件隔離開。 編寫自定義的 Mock 物件需要額外的編碼工作,同時也可能引入錯誤。EasyMock 提供了根據指定介面動態構建 Mock 物件的方法,避免了手工編寫 M

神經網路系列之四--線性迴歸方法原理

系列部落格,原文在筆者所維護的github上:https://aka.ms/beginnerAI, 點選star加星不要吝嗇,星越多筆者越努力 第4章 單入單出的單層神經網路 4.0 單變數線性迴歸問題 4.0.1 提出問題 在網際網路建設初期,各大運營商需要解決的問題就是保證伺服器所在的機房的溫度常年保持

神經網路系列之五 -- 線性二分類的方法原理

系列部落格,原文在筆者所維護的github上:https://aka.ms/beginnerAI, 點選star加星不要吝嗇,星越多筆者越努力。 第6章 多入單出的單層神經網路 6.0 線性二分類 6.0.1 提出問題 我們經常看到中國象棋棋盤中,用楚河漢界分割開了兩個陣營的棋子。回憶歷史,公元前206年前後

threading.local()使用原理剖析

threading.local()使用與原理剖析 前言   還是第一次摘出某個方法來專門寫一篇隨筆,哈哈哈。   為什麼要寫這個方法呢?因為它確實太重要了,包括後期的Flask框架原始碼中都有它的影子。   那麼我們就來瞄一眼這個東西是啥吧。   作用   在Python官方中文文件中(Py

NIO原理剖析Netty初步----淺談高性能服務器開發(一)

返回 創建 基於 register 訪問 io操作 nbsp info class 除特別註明外,本站所有文章均為原創,轉載請註明地址 在博主不長的工作經歷中,NIO用的並不多,由於使用原生的Java NIO編程的復雜性,大多數時候我們會選擇Netty,m

Spring Boot 揭秘實戰 源碼分析 - 工作原理剖析

pro rop 功能 row commons 擴展 onf 公眾 ica 文章目錄 1. EnableAutoConfiguration 幫助我們做了什麽 2. 配置參數類 – FreeMarkerProperties 3. 自動配置類 – FreeMarkerAuto

下載ASP.NET MVC5框架剖析案例解析(MVC5原理剖析、漏洞及運維安全、設計模式)

mvc5框架剖析與案例解析 運維安全 mvc5原理剖析 地址:http://pan.baidu.com/s/1dFhBu2d 密碼:peas轉一播放碼,200多課!本課程針對MVC5版本的ASP.NET MVC,同時涉及太多底層實現的內容,所以大部分是找不到現成參考資料的,這些內容大都來自講師對源

Spark2.1內部原理剖析源碼閱讀、程序設計企業級應用案例

封裝 以及 url string 計算機網絡 內部原理 企業級 目標 sql 1、本文目標以及其它說明: 本文或者本次系列主要是弄清楚spark.2.2.0版本中,spark core 包下rpc通信情況。從源代碼上面看到,底層通信是用的netty,因為本系

斷點續傳的原理剖析例項講解

斷點續傳的原理剖析與例項講解   本文所要講的是Android斷點續傳的內容,以例項的形式進行了詳細介紹。 一、斷點續傳的原理        其實斷點續傳的原理很簡單,就是在http的請求上和一般的下載有所不同而已。    

分享《深入淺出深度學習:原理剖析python實踐》PDF+源代碼

img color fff png aid pdf ffffff pytho 下載 下載:https://pan.baidu.com/s/1H4N0W5sPOE7YlK0KyC7TZQ 更多資料分享:http://blog.51cto.com/3215120 《深入淺出深度

《深入淺出深度學習:原理剖析python實踐》pdf 下載

深入淺出深度學習:原理剖析與Python實踐》介紹了深度學習相關的原理與應用,全書共分為三大部分,第一部分主要回顧了深度學習的發展歷史,以及Theano的使用;第二部分詳細講解了與深度學習相關的基礎知識,包括線性代數、概率論、概率圖模型、機器學習和至優化演算法;在第三部分中,針對若干核心的深度

Java中的顯示鎖ReentrantLock使用AbstractQueuedSynchronizer原理剖析

考慮一個場景,輪流列印0-100以內的技術和偶數。通過使用 synchronize 的 wait,notify機制就可以實現,核心思路如下: 使用兩個執行緒,一個列印奇數,一個列印偶數。這兩個執行緒會共享一個數據,資料每次自增,當列印奇數的執行緒發現當前要列印的數字不是奇數時,執行等待,否則列印奇數,並將數字

輸入流當中的read方法和readfully方法的區別原理

原文連結:https://blog.csdn.net/yangjingyuan/article/details/6151234?locationNum=3 DataInputStream類中的read(byte[] b)和readFully(byte[] b)讀取訊息到底有什麼區別呢?

HBase LRUBlockCacheBucketCache二級快取機制原理剖析引數調優-OLAP商業環境實戰

本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。版權宣告:禁止轉載,歡迎學習。QQ郵箱地址:[email protected],如有任何學術交流,可隨時聯絡。

課時17 第三課Spark內部原理剖析原始碼閱讀(五)

為何spark shuffle比mapreduce shuffle慢? 主要是spark shuffle的shuffle read階段還不夠優秀,它是基於hashmap實現的,shuffle read會把shuffel write階段已經排序資料給重新轉成亂序的,轉成亂序之後又做了排序,導致非常低效,sp

高併發架構分散式技術NoSQL -- Redis原理剖析

首先奉獻出微信 java後端技術 公眾號裡的學習腦圖,接下來的內容將會按照該圖進行自學梳理。redis原理剖析Redis是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫。它可以用作資料庫、快取和訊息中介軟體。 /* *

HBase LRUBlockCacheBucketCache二級快取機制原理剖析引數調優

本文章來自轉載,轉載地址:https://juejin.im/post/5bfd666a6fb9a049ea38a55a     在此需要著重感謝華為的架構師秦凱新大佬 1 BlockCache 唯一性 一個 RegionServer只有一個BlockCach

DevOps:原理方法實踐 – 運維派

前言/序言 近年來DevOps開發模式對軟體產業產生了深遠影響,相當多的軟體企業開始採用這種新的模式。來自權威機構的預測報告甚至認為,未來全球排名前2000的軟體企業中,超過80%都將轉向DevOps模式。事實上,DevOps發展速度之快和影響範圍之廣都大大超出了人們的預期。 DevOps之所以會

二十五、併發程式設計之join應用實現原理剖析

1、join有什麼用呢? 當一個執行緒正在進行中的時候,如果我們想呼叫另外一個執行緒的話,這時我們可以使用join。 2、join方法的底層原理,簡單來說就是,join方法能把所呼叫join方法的執行緒進入休眠狀態(wait()),等執行完joinThread執行緒之後,會自動

java0xff進行&運算的原理剖析

在剖析該問題前請看如下程式碼 public static String bytes2HexString(byte[] b) {   String ret = "";   for (int i = 0; i < b.length; i++) {    Stri