無論是敏捷開發、持續交付,還是測試驅動開發(TDD)都把單元測試作為實現的基石。隨着這些先進的編程開發模式日益深入人心,單元測試如今顯得越來越重要了。在敏捷開發、持續交付中要求單元測試一定要快(不能訪問實際的文件系統或數據庫),而TDD經常會碰到協同模塊尚未開發的情況,而mock技術正是解決這些問題的靈丹妙葯。
mock技術的目的和作用是模擬一些在應用中不容易構造或者比較復雜的對象,從而把測試與測試邊界以外的對象隔離開。
我們可以自己編寫自定義的Mock對象實現mock技術,但是編寫自定義的Mock對象需要額外的編碼工作,同時也可能引入錯誤。現在實現mock技術的優秀開源框架有很多,本文對幾個典型的mock測試框架作了簡明介紹,希望對大家有所幫助。
1.EasyMock
EasyMock 是早期比較流行的MocK測試框架。它提供對接口的模擬,能夠通過錄制、回放、檢查三步來完成大體的測試過程,可以驗證方法的調用種類、次數、順序,可以令 Mock 對象返回指定的值或拋出指定異常。通過 EasyMock,我們可以方便的構造 Mock 對象從而使單元測試順利進行。
EasyMock 是采用 MIT license 的一個開源項目,可以在 Sourceforge 上下載到。(http://sourceforge.net/projects/easymock/files/EasyMock/)
如果使用maven也可以如下引入:
<dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>3.1</version> <scope>test</scope> </dependency>
使用EasyMock大致可以划分為以下幾個步驟:
① 使用 EasyMock 生成 Mock 對象;
② 錄制 Mock 對象的預期行為和輸出;
③ 將 Mock 對象切換到 播放 狀態;
④ 調用 Mock 對象方法進行單元測試;
⑤ 對 Mock 對象的行為進行驗證。
現在用一個例子來簡單呈現以上的步驟,假設有一個類需要被模擬的類如下:
public class Class1Mocked {
public String hello(String name){
System.out.println("hello "+name);
return "hello "+name;
}
public void show(){
System.out.println("Class1Mocked.show()");
}
}
首先靜態導入EasyMock的方法:
import static org.easymock.EasyMock.*;
例1.1 EasyMock第一個例子
@Test
public void testMockMethod() {
Class1Mocked obj = createMock(Class1Mocked.class);①
expect(obj.hello("z3")).andReturn("hello l4");②
replay(obj);③
String actual = obj.hello("z3");④
assertEquals("hello l4", actual);
verify(obj);⑤
}
在⑤驗證階段中,會嚴格驗證mock對象是否按錄制的行為如期發生(包括執行的順序及次數)。
2.mockito
EasyMock之后流行的mock工具。相對EasyMock學習成本低,而且具有非常簡潔的API,測試代碼的可讀性很高。
mockito可以在https://code.google.com/p/mockito/上下載,如果使用maven可以如下引入:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> <scope>test</scope> </dependency>
使用mockito大致可以划分為以下幾個步驟:
① 使用 mockito 生成 Mock 對象;
② 定義(並非錄制) Mock 對象的行為和輸出(expectations部分);
③ 調用 Mock 對象方法進行單元測試;
④ 對 Mock 對象的行為進行驗證。
現在用一個例子來簡單呈現以上的步驟:
首先靜態導入mockito的方法:
import static org.mockito.Mockito.*;
例2.1 mockito第一個例子
@Test
public void testMockMethod() {
Class1Mocked obj=mock(Class1Mocked.class);①
when(obj.hello("z3")).thenReturn("hello l4");②
String actual=obj.hello("z3");③
assertEquals("hello l4",actual);
verify(obj).hello("z3");④
//verify(obj,times(1)).hello("z3"); //可以加參數驗證次數
}
可以看到與EasyMock相比,少了切換到播放狀態一步。這是很自然的,本來就不是錄制而談播放呢,而在驗證階段可以通過增加參數(time(int)、atLeastOnce()、atLeast(int)、never()等)來精確驗證調用次數。
而如果要驗證調用順序可以如下控制:
例2.2 驗證順序
@Test
public void testMockMethodInOrder() {
Class1Mocked objOther = mock(Class1Mocked.class);
Class1Mocked objCn = mock(Class1Mocked.class);
when(objOther.hello("z3")).thenReturn("hello l4");
when(objCn.hello("z3")).thenReturn("hello 張三");
String other = objOther.hello("z3");
assertEquals("hello l4", other);
String cn = objCn.hello("z3");
assertEquals("hello 張三", cn);
InOrder inOrder = inOrder(objOther, objCn); //此行並不決定順序,下面的兩行才開始驗證順序
inOrder.verify(objOther).hello("z3");
inOrder.verify(objCn).hello("z3");
}
在之前的介紹的模擬操作中,我們總是去模擬一整個類或者對象,對於沒有使用 When().thenReturn()方法指定的函數,系統會返回各種類型的默認值(具體值可參考官方文檔)。而局部模擬創建出來的模擬對象依然是原系統對象,雖然可以使用方法When().thenReturn()來指定某些具體方法的返回值,但是沒有被用此函數修改過的函數依然按照系統原始類的方式來執行,下面對非局部模擬和局部模擬分別舉例來說明:
例2.3 非局部模擬
@Test
public void testSkipExpect() {
Class1Mocked obj = mock(Class1Mocked.class);
assertEquals(null, obj.hello("z3"));
obj.show();
verify(obj).hello("z3");
verify(obj).show();
}
上面的代碼省略了expectations部分(即定義代碼行為和輸出),運行該測試可以看到hello方法默認返回null(show方法本來就是無返回值的),而且在控制台中兩個方法都沒有輸出任何語句。
mockito的局部模擬有兩種方式,一種是doCallRealMethod()方式,另一種是spy()方式。
例2.4 局部模擬doCallRealMethod ()方式
@Test
public void testCallRealMethod () {
Class1Mocked obj = mock(Class1Mocked.class);
doCallRealMethod().when(obj).hello("z3");
assertEquals("hello z3",obj.hello("z3"));
assertEquals(null,obj.hello("l4"));
obj.show();
verify(obj).hello("z3");
verify(obj).hello("l4");
verify(obj).show();
}
運行這個測試會發現在執行hello("z3")時會執行原有的代碼,而執行hello("l4")時則是返回默認值null且沒有輸出打印,執行show()同樣沒有輸出打印。
例2.5 局部模擬spy()方式
@Test
public void testSpy() {
Class1Mocked obj = spy(new Class1Mocked());
doNothing().when(obj).show();
assertEquals("hello z3",obj.hello("z3"));
obj.show();
verify(obj).hello("z3");
verify(obj).show();
}
運行這個測試會發現在執行hello("z3")時會執行原有的代碼,但是執行show()時在控制台中沒有打印語句。
但值得注意的是在mockito的psy()方式模擬中expectations部分使用的語法不同,執行起來存在微妙的不同,如下:
例2.6 值得注意的“陷阱”
@Test
public void testSpy2() {
Class1Mocked obj = spy(new Class1Mocked());
when(obj.hello("z3")).thenReturn("hello l4");
assertEquals("hello l4",obj.hello("z3"));
verify(obj).hello("z3");
}
上面的代碼雖然能順利運行,但在控制台中輸出了hello z3,說明實際的代碼仍然執行了,只是mockito在最后替換了返回值。但下面的代碼就不會執行實際的代碼:
@Test
public void testSpy3() {
Class1Mocked obj = spy(new Class1Mocked());
doReturn("hello l4").when(obj).hello("z3");
assertEquals("hello l4",obj.hello("z3"));
verify(obj).hello("z3");
}
3.PowerMock
這個工具是在EasyMock和Mockito上擴展出來的,目的是為了解決EasyMock和Mockito不能解決的問題,比如對static, final, private方法均不能mock。其實測試架構設計良好的代碼,一般並不需要這些功能,但如果是在已有項目上增加單元測試,老代碼有問題且不能改時,就不得不使用這些功能了。
PowerMock 在擴展功能時完全采用和被擴展的框架相同的 API, 熟悉 PowerMock 所支持的模擬框架的開發者會發現 PowerMock 非常容易上手。PowerMock 的目的就是在當前已經被大家所熟悉的接口上通過添加極少的方法和注釋來實現額外的功能。目前PowerMock 僅擴展了 EasyMock 和 mockito,需要和EasyMock或Mockito配合一起使用。
PowerMock可以在https://code.google.com/p/powermock/上下載,本文以PowerMock+mockito為例,使用maven的話,添加如下依賴即可,maven會自動引入mockito的包。
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>1.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.5</version> <scope>test</scope> </dependency>
現在舉例來說明PowerMock的使用,假設有一個類需要被模擬的類如下:
public class Class2Mocked {
public static int getDouble(int i){
return i*2;
}
public String getTripleString(int i){
return multiply3(i)+"";
}
private int multiply3(int i){
return i*3;
}
}
首先靜態導入PowerMock的方法:
import static org.powermock.api.mockito.PowerMockito.*;
然后在使用junit4的測試類上做如下聲明:
@RunWith(PowerMockRunner.class)
@PrepareForTest( { Class2Mocked.class })
例3.1 模擬靜態方法
@Test
public void testMockStaticMethod() {
mockStatic(Class2Mocked.class);
when(Class2Mocked.getDouble(1)).thenReturn(3);
int actual = Class2Mocked.getDouble(1);
assertEquals(3, actual);
verifyStatic();
Class2Mocked.getDouble(1);
}
PowerMockit的局域模擬使用方式和mockito類似(畢竟是擴展mockito),但強大之處在於可以模擬private方法,普通方法和final方法。模擬普通方法和final方法的方式與模擬private方法一模一樣,現以模擬private方法為例。
例3.2 模擬私有方法(doCallRealMethod方式)
@Test
public void testMockPrivateMethod() throws Exception {
Class2Mocked obj = mock(Class2Mocked.class);
when(obj, "multiply3", 1).thenReturn(4);
doCallRealMethod().when(obj).getTripleString(1);
String actual = obj.getTripleString(1);
assertEquals("4", actual);
verifyPrivate(obj).invoke("multiply3", 1);
}
例3.3 模擬私有方法(spy方式)
@Test
public void testMockPrivateMethod2() throws Exception {
Class2Mocked obj = spy(new Class2Mocked());
when(obj, "multiply3", 1).thenReturn(4);
String actual = obj.getTripleString(1);
assertEquals("4", actual);
verifyPrivate(obj).invoke("multiply3", 1);
}
除此之外,PowerMock也可以模擬構造方法,如下所示:
例3.4 模擬構造方法
@Test
public void testStructureWhenPathDoesntExist() throws Exception {
final String directoryPath = "mocked path";
File directoryMock = mock(File.class);
whenNew(File.class).withArguments(directoryPath).thenReturn(directoryMock);
when(directoryMock.exists()).thenReturn(true);
File file=new File(directoryPath);
assertTrue(file.exists());
verifyNew(File.class).withArguments(directoryPath);
verifyPrivate(directoryMock).invoke("exists");
}
4.Jmockit
JMockit 是一個輕量級的mock框架是用以幫助開發人員編寫測試程序的一組工具和API,該項目完全基於 Java 5 SE 的 java.lang.instrument 包開發,內部使用 ASM 庫來修改Java的Bytecode。
Jmockit功能和PowerMock類似,某些功能甚至更為強大,但個人感覺其代碼的可讀性並不強。
Jmockit可以在https://code.google.com/p/jmockit/上下載,使用maven的話添加如下依賴即可:
<dependency> <groupId>com.googlecode.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.0</version> <scope>test</scope> </dependency>
Jmockit也可以分類為非局部模擬與局部模擬,區分在於Expectations塊是否有參數,有參數的是局部模擬,反之是非局部模擬。而Expectations塊一般由Expectations類和NonStrictExpectations類定義。用Expectations類定義的,則mock對象在運行時只能按照 Expectations塊中定義的順序依次調用方法,不能多調用也不能少調用,所以可以省略掉Verifications塊;而用NonStrictExpectations類定義的,則沒有這些限制,所以如果需要驗證,則要添加Verifications塊。
現在舉例說明Jmockit的用法:
例4.1 非局部模擬Expectations類定義
@Mocked //用@Mocked標注的對象,不需要賦值,jmockit自動mock
Class1Mocked obj;
@Test
public void testMockNormalMethod1() {
new Expectations() {
{
obj.hello("z3");
returns("hello l4", "hello w5");
obj.hello("張三");
result="hello 李四";
}
};
assertEquals("hello l4", obj.hello("z3"));
assertEquals("hello w5", obj.hello("z3"));
assertEquals("hello 李四", obj.hello("張三"));
try {
obj.hello("z3");
} catch (Throwable e) {
System.out.println("第三次調用hello(\"z3\")會拋出異常");
}
try {
obj.show();
} catch (Throwable e) {
System.out.println("調用沒有在Expectations塊中定義的方法show()會拋出異常");
}
}
例4.2 非局部模擬 NonStrictExpectations類定義
public void testMockNormalMethod2() {
new NonStrictExpectations() {
{
obj.hello("z3");
returns("hello l4", "hello w5");
}
};
assertEquals("hello l4", obj.hello("z3"));
assertEquals("hello w5", obj.hello("z3"));
assertEquals("hello w5", obj.hello("z3"));// 會返回在NonStrictExpectations塊中定義的最后一個返回值
obj.show();
new Verifications() {
{
obj.hello("z3");
times = 3;
obj.show();
times = 1;
}
};
}
運行這個測試會發現show()方法沒有在控制台輸出打印語句,說明是Jmockit對show方法也進行了默認mock。
例4.3 局部模擬
@Test
public void testMockNormalMethod() throws IOException {
final Class1Mocked obj = new Class1Mocked();//也可以不用@Mocked標注,但需要final關鍵字
new NonStrictExpectations(obj) {
{
obj.hello("z3");
result = "hello l4";
}
};
assertEquals("hello l4", obj.hello("z3"));
assertEquals("hello 張三", obj.hello("張三"));
new Verifications() {
{
obj.hello("z3");
times = 1;
obj.hello("張三");
times = 1;
}
};
}
運行這個測試發現hello("z3")返回由Expectations塊定義的值,但hello("張三")執行的是實際的代碼。
例4.4 模擬靜態方法
@Test
public void testMockStaticMethod() {
new NonStrictExpectations(Class2Mocked.class) {
{
Class2Mocked.getDouble(1);
result = 3;
}
};
assertEquals(3, Class2Mocked.getDouble(1));
new Verifications() {
{
Class2Mocked.getDouble(1);
times = 1;
}
};
}
例4.5 模擬私有方法
@Test
public void testMockPrivateMethod() throws Exception {
final Class2Mocked obj = new Class2Mocked();
new NonStrictExpectations(obj) {
{
this.invoke(obj, "multiply3", 1);
result = 4;
}
};
String actual = obj.getTripleString(1);
assertEquals("4", actual);
new Verifications() {
{
this.invoke(obj, "multiply3", 1);
times = 1;
}
};
}
例4.6 設置私有屬性的值
假設有一個類需要被模擬的類如下:
public class Class3Mocked {
private String name = "name_init";
public String getName() {
return name;
}
private static String className="Class3Mocked_init";
public static String getClassName(){
return className;
}
public static int getDouble(int i){
return i*2;
}
public int getTriple(int i){
return i*3;
}
}
如下可以設置私有屬性的值:
@Test
public void testMockPrivateProperty() throws IOException {
final Class3Mocked obj = new Class3Mocked();
new NonStrictExpectations(obj) {
{
this.setField(obj, "name", "name has bean change!");
}
};
assertEquals("name has bean change!", obj.getName());
}
例4.7 設置靜態私有屬性的值
@Test
public void testMockPrivateStaticProperty() throws IOException {
new NonStrictExpectations(Class3Mocked.class) {
{
this.setField(Class3Mocked.class, "className", "className has bean change!");
}
};
assertEquals("className has bean change!", Class3Mocked.getClassName());
}
例4.8 改寫普通方法的內容
@Test
public void testMockNormalMethodContent() throws IOException {
final Class3Mocked obj = new Class3Mocked();
new NonStrictExpectations(obj) {
{
new MockUp<Class3Mocked>() {
@Mock
public int getTriple(int i) {
return i * 30;
}
};
}
};
assertEquals(30, obj.getTriple(1));
assertEquals(60, obj.getTriple(2));
}
例4.9 改寫靜態方法的內容
如果要改寫Class3Mocked類的靜態getDouble方法,則需要新建一個類含有與getDouble方法相同的函數聲明,並且用@Mock標注,如下:
public class Class4Mocked {
@Mock
public static int getDouble(int i){
return i*20;
}
}
如下即可改寫:
@Test
public void testDynamicMockStaticMethodContent() throws IOException {
Mockit.setUpMock(Class3Mocked.class, Class4Mocked.class);
assertEquals(20, Class3Mocked.getDouble(1));
assertEquals(40, Class3Mocked.getDouble(2));
}

