EasyMock入門使用
一、EasyMock安裝
-
EasyMock是一套通過簡單的方法對於指定的接口或類生成Mock對象的類庫,它能利用對接口或類的模擬來輔助單元測試。
-
要使用EasyMock輔助單元測試,添加easymock的jar包即可。
二、EasyMock使用
通過EasyMock,我們可以為指定的接口動態地創建Mock對象,並利用Mock對象來模擬協同模塊或領域對象,從而使單元測試順利進行。此過程可划分為以下步驟:
- 使用EasyMock生成Mock對象
- 設定Mock對象的預期行為和輸出
- 將Mock對象切換到Replay狀態
- 調用Mock對象方法進行單元測試
- 對Mock對象的行為進行驗證
2.1 使用EasyMock生成Mock對象
根據指定的接口或類,EasyMock能動態地創建Mock對象。
2.1.1 為接口生成Mock對象
Eg:以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 對象來解決這個問題。
static import org.easymock.Easymock; //靜態方法引入
ResultSet mockResultSet = createMock(ResultSet.class);
或
import org.easymock.Easymock;
ResultSet mockResultSet = Easymock.createMock(ResultSet.class);
2.1.2 為類生成Mock對象
EasyMock默認只支持為接口生成Mock對象,若要為類生成Mock對象,需下載擴展包 EasyMock Class Extension
,在對具體類進行模擬時,只需把 org.easymock.EasyMock
替換為 org.easymock.classextension.Easymock
。
2.1.3 使用IMocksControl對象管理Mock對象
若在相對復雜的測試用例中使用多個Mock對象,可使用EasyMock提供的生成和管理Mock對象的機制。
EasyMock類的createControl
方法能創建一個接口IMocksControl
的對象,此對象能創建並管理多個Mock對象。
Eg:
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);
2.2 設定Mock對象的預期行為和輸出
-
在一個完整測試中,一個Mock對象會經歷兩個狀態:Record狀態和Replay狀態。
Mock對象一經創建,狀態為Record,Record狀態下用戶可以設定Mock對象的預期行為和輸出,這些對象行為會被錄制下來,保存在Mock對象中。
-
添加Mock對象行為的過程分為三步:
- 對Mock對象的特定方法作出調用
- 通過org.easymock.EasyMock提供的靜態方法expectLastCall獲取上一次方法調用所對應的IExpectionSetters實例
- 用過IExpectionSetters實例設定Mock對象的預期輸出(有兩種類型)
- 產生返回值
- 拋出異常
(Mock對象的行為可簡單理解為Mock對象的調用和方法調用所產生的輸出。)
2.2.1 設定預期返回值
設定返回值對應接口IExpectionSetters
的andReturn
方法。
IExpectationSetters<T> andReturn(T value);
2.2.1.1 返回值不是void
-
Eg:仍用
ResultSet
接口的 Mock 對象為例,若希望方法mockResultSet.getString(1)
的返回值為 "My return value",則:mockResultSet.getString(1); EasyMock.expectLastCall().andReturn("My return value");
或:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value");
-
若希望 某方法的調用總是返回一個相同的值 ,為避免每次調用都為Mock對象的行為進行一次設定,可用默認返回值的方法——
andSubReturn
方法。void andStubReturn(Object value);
Eg:假設我們創建了
Statement
和ResultSet
接口的 Mock 對象 mockStatement 和 mockResultSet,在測試過程中,我們希望 mockStatement 對象的executeQuery
方法總是返回 mockResultSet,則:mockStatement.executeQuery("SELECT * FROM sales_order_table"); EasyMock.expectLastCall().andStubReturn(mockResultSet);
或:
EasyMock.expect(mockStatement.executeQuery("SELECT * FROM sales_order_table")).andStubReturn(mockResultSet);
2.2.1.2 返回值為void
若方法的返回值類型為void,則對於此類方法,我們無需設定返回值,只需設置調用次數就可以。(也可以不設置)
How設定調用次數,詳看2.2.3。
Eg:以 ResultSet
接口的 close
方法為例,假設在測試過程中,該方法被調用3至5次,則:
mockResultSet.close();
EasyMock.expectLastCall().times(3,5);// 最新版本的EasyMock可以忽略此句
或:
EasyMock.expect(mockResult.close()).times(3, 5);
2.2.2 設定預期異常拋出
設定預期拋出異常對應接口IExpectionSetters
的andThrow
方法。
IExpectationSetters<T> andThrow(Throwable throwable);
類似的,設定拋出默認異常對應接口IExpectionSetters
的andStubThrow
方法。
void andStubThrow(Throwable throwable);
Eg:
EasyMock.expectLastCall().andThrow(
new MyException(new RuntimeException())).anyTimes();
2.2.3 設定預期方法調用次數
- 設定確定的調用次數:通過接口
IExpectionSetters
的times
方法。
IExpectationSetters<T>times(int count);
Eg:我們希望 mockResultSet 的 getString
方法在測試過程中被調用3次,期間的返回值都是 "My return value",則:
mockResultSet.getString(1);
expectLastCall().andReturn("My return value").times(3);
或:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value").times(3);
- 設定非准確調用次數:
times(int minTimes, int maxTimes)
:該方法最少被調用 minTimes 次,最多被調用 maxTimes 次。atLeastOnce()
:該方法至少被調用一次。anyTimes()
:該方法可以被調用任意次。
2.2.4 若返回結果在運行時才能確定
很可能某個方法期望的返回結果不是固定的,例如根據傳入參數不同而不同;這時需要使用andAnswer()方法:
EasyMock.expect(mockService.execute(EasyMock.anyInt())).andAnswer(new IAnswer<Integer>() {
public Integer answer() throws Throwable {
Integer count = (Integer) EasyMock.getCurrentArguments()[0];
return count * 2;
}
});
{% note warning %}
注意:通過EasyMock.getCurrentArguments()
可以獲取傳入參數!
{% endnote %}
2.3 將Mock對象切換到Replay狀態——replay
-
在生成Mock對象和設定Mock對象行為的兩個階段,Mock對象的狀態均為Record,此階段Mock對象會記錄用戶對預期行為和輸出的設定。
-
在使用Mock對象隱形實際的測試前,需將Mock對象的狀態切換為Replay,此階段Mock對象能根據設定對特定的方法調用作出預期的響應。
-
將對象切換到Replay狀態有兩種方法:
-
若Mock對象是通過createMock方法生成
EasyMock.replay(mockResultSet);
-
若Mock對象通過createControl方法生成的接口對象control的createMock方法生成
control.replay();
此語句可將通過coltrol的createMock方法生成的所有Mock對象均切換為Replay狀態。
-
2.4 調用Mock對象方法進行單元測試
此部分放到JUnit+EasyMock實例中。
2.5 對Mock對象的行為進行驗證——verify
在利用Mock對象進行實際的測試過程后,還需對Mock對象的方法調用的次數進行驗證。
-
若Mock對象是通過createMock方法生成
EasyMock.verify(mockResultSet);
-
若Mock對象通過createControl方法生成的接口對象control的createMock方法生成
control.verify();
同理,此語句可驗證通過coltrol的createMock方法生成的所有Mock對象方法的調用次數。
2.6 Mock對象的重用——reset
為避免生成過多的Mock對象,EasyMock允許對原有的Mock對象進行重用。
可使用reset方法對Mock對象重新初始化。重新初始化后,Mock對象被置為Record狀態。
-
若Mock對象是通過createMock方法生成
EasyMock.reset(mockResultSet);
-
若Mock對象通過createControl方法生成的接口對象control的createMock方法生成
control.reset();
同理,此語句可驗證通過coltrol的createMock方法生成的所有Mock對象重新初始化。
三、在EasyMock中使用參數匹配器
使用Mock對象進行實際的測試過程中,EasyMock會根據方法名和參數來匹配一個預期方法的調用。
此時,EasyMock對參數的匹配默認使用equals()方法進行比較,這可能會引起一些問題,因此EasyMock提供了一些參數匹配方式。
{% note info %}
如2.2.1.1中創建的mockStatement對象:
mockStatement.executeQuery("SELECT * FROM sales_order_table");
EasyMock.expectLastCall().andStubReturn(mockResultSet);
實際調用中,可能會遇到SQL語句中某些關鍵詞大小寫問題(因為SQL語句不區分大小寫)如將SELECT寫成select,此時EasyMock采用的默認匹配器equals方法將認為參數不匹配,則Mock對象的預期方法不會被調用。
{% endnote %}
3.1 EasyMock預定義的參數匹配器
anyObject()
:任意輸入值都與預期值匹配;aryEq(X value)
:通過Arrays.equals()
進行匹配,適用於數組對象;isNull()
:當輸入值為Null時匹配;notNull()
:當輸入值不為Null時匹配;same(X value)
:當輸入值和預期值是同一個對象時匹配;lt(X value), leq(X value), geq(X value), gt(X value)
:當輸入值小於、小等於、大等於、大於預期值時匹配,適用於數值類型;startsWith(String prefix), contains(String substring), endsWith(String suffix)
:當輸入值以預期值開頭、包含預期值、以預期值結尾時匹配,適用於String類型;matches(String regex)
:當輸入值與正則表達式匹配時匹配,適用於String類型。
例如:若我對mockStatement具體執行的語句並不關注,希望所有輸入的字符串都能夠匹配這一方法的調用
EasyMock.expect(mockStatement.executeQuery(anyObject())).andStubReturn(mockResultSet);
詳細參閱 easymock教程-參數匹配
3.2 EasyMock自定義參數匹配器
-
預定義的參數匹配器無法滿足一些復雜情況,此時需自己定義參數匹配器。
如:在3.1中我們希望有一個匹配器對SQL中關鍵詞的大小寫不敏感,使用anyObject其實並不好,此時我們可以自定義參數匹配器SQLEquals。
-
How自定義參數匹配器:
-
實現
org.easymock.IArgumentMatcher
接口-
matches(Object actual)
方法應當實現輸入值和預期值的匹配邏輯 -
appendTo(StringBuffer buffer)
方法可以添加當匹配失敗時需要顯示的信息
-
-
使用靜態方法包裝實現接口的類
package org.easymock.demo.matcher; import static org.easymock.EasyMock.reportMatcher; import org.easymock.IArgumentMatcher; //實現IArgumentMatcher接口 public class SQLEquals implements IArgumentMatcher { private String expectedSQL = null; public SQLEquals(String expectedSQL) { this.expectedSQL = expectedSQL; } //當匹配失敗時需要顯示的信息 public void appendTo(StringBuffer buffer) { buffer.append("SQLEquals(\"" + expectedSQL + "\")"); } //輸入值和預期值的匹配邏輯 public boolean matches(Object actualSQL) { if (actualSQL == null && expectedSQL == null) return true; else if (actualSQL instanceof String) return expectedSQL.equalsIgnoreCase((String) actualSQL); else return false; } //自定義參數匹配器SQLEquals靜態方法 public static String sqlEquals(String in) { reportMatcher(new SQLEquals(in)); return in; } }
使用自定義的sqlEquals匹配器:
EasyMock.expect(mockStatement.executeQuery(sqlEquals("SELECT * FROM sales_order_table"))).andStubReturn(mockResultSet);
-
四、特殊的Mock對象類型
上述創建的Mock對象都屬於EasyMock默認的Mock對象類型,它對預期方法的調用順序不敏感,對非預期的方法調用拋出AssertionError。
除此默認類型,EasyMock還提供一些特殊的Mock類型用於支持不同的需求。
4.1 Strick Mock對象
Stick Mock對象——對方法調用的先后順序敏感,創建方法如下:
-
使用
EasyMock.createStrickMock()
來創建:ResultSet strickMockResultSet = EasyMock.createStrickMock(ResultSet.class);
-
類似於createMock,同樣可用
IMocksControl
實例來創建一個Stick Mock對象:IMocksControl control = EasyMock.createStrictControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class);
4.2 Nice Mock對象
Nice Mock對象——默認返回0,null或false等“無效值”,創建方法如下:
-
使用
EasyMock.createNiceMock()
來創建:ResultSet strickMockResultSet = EasyMock.createNiceMock(ResultSet.class);
-
類似於createMock,同樣可用
IMocksControl
實例來創建一個Nice Mock對象:IMocksControl control = EasyMock.createNiceControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class);