使用強大的 Mockito 測試框架來測試你的程式碼
這篇教程介紹瞭如何使用 Mockito 框架來給軟體寫測試用例
1. 預備知識
如果需要往下學習,你需要先理解 Junit 框架中的單元測試。
2. 使用mock物件來進行測試
2.1. 單元測試的目標和挑戰
單元測試的思路是在不涉及依賴關係的情況下測試程式碼(隔離性),所以測試程式碼與其他類或者系統的關係應該儘量被消除。一個可行的消除方法是替換掉依賴類(測試替換),也就是說我們可以使用替身來替換掉真正的依賴物件。
2.2. 測試類的分類
dummy object 做為引數傳遞給方法但是絕對不會被使用。譬如說,這種測試類內部的方法不會被呼叫,或者是用來填充某個方法的引數。
Fake 是真正介面或抽象類的實現體,但給物件內部實現很簡單。譬如說,它存在記憶體中而不是真正的資料庫中。(譯者注:Fake 實現了真正的邏輯,但它的存在只是為了測試,而不適合於用在產品中。)
stub 類是依賴類的部分方法實現,而這些方法在你測試類和介面的時候會被用到,也就是說 stub 類在測試中會被例項化。stub 類會迴應任何外部測試的呼叫。stub 類有時候還會記錄呼叫的一些資訊。
mock object 是指類或者介面的模擬實現,你可以自定義這個物件中某個方法的輸出結果。
測試替代技術能夠在測試中模擬測試類以外物件。因此你可以驗證測試類是否響應正常。譬如說,你可以驗證在 Mock 物件的某一個方法是否被呼叫。這可以確保隔離了外部依賴的干擾只測試測試類。
我們選擇 Mock 物件的原因是因為 Mock 物件只需要少量程式碼的配置。
2.3. Mock 物件的產生
你可以手動建立一個 Mock 物件或者使用 Mock 框架來模擬這些類,Mock 框架允許你在執行時建立 Mock 物件並且定義它的行為。
一個典型的例子是把 Mock 物件模擬成資料的提供者。在正式的生產環境中它會被實現用來連線資料來源。但是我們在測試的時候 Mock 物件將會模擬成資料提供者來確保我們的測試環境始終是相同的。
Mock 物件可以被提供來進行測試。因此,我們測試的類應該避免任何外部資料的強依賴。
通過 Mock 物件或者 Mock 框架,我們可以測試程式碼中期望的行為。譬如說,驗證只有某個存在 Mock 物件的方法是否被呼叫了。
2.4. 使用 Mockito 生成 Mock 物件
Mockito 是一個流行 mock 框架,可以和JUnit結合起來使用。Mockito 允許你建立和配置 mock 物件。使用Mockito可以明顯的簡化對外部依賴的測試類的開發。
一般使用 Mockito 需要執行下面三步
-
模擬並替換測試程式碼中外部依賴。
-
執行測試程式碼
-
驗證測試程式碼是否被正確的執行
3. 為自己的專案新增 Mockito 依賴
3.1. 在 Gradle 新增 Mockito 依賴
如果你的專案使用 Gradle 構建,將下面程式碼加入 Gradle 的構建檔案中為自己專案新增 Mockito 依賴
repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.0.57-beta" }
3.2. 在 Maven 新增 Mockito 依賴
需要在 Maven 宣告依賴,您可以在 http://search.maven.org 網站中搜索 g:"org.mockito", a:"mockito-core" 來得到具體的宣告方式。
3.3. 在 Eclipse IDE 使用 Mockito
Eclipse IDE 支援 Gradle 和 Maven 兩種構建工具,所以在 Eclipse IDE 新增依賴取決你使用的是哪一個構建工具。
3.4. 以 OSGi 或者 Eclipse 外掛形式新增 Mockito 依賴
在 Eclipse RCP 應用依賴通常可以在 p2 update 上得到。Orbit 是一個很好的第三方倉庫,我們可以在裡面尋找能在 Eclipse 上使用的應用和外掛。
4. 使用Mockito API
4.1. 靜態引用
如果在程式碼中靜態引用了org.mockito.Mockito.*;
,那你你就可以直接呼叫靜態方法和靜態變數而不用建立物件,譬如直接呼叫 mock() 方法。
4.2. 使用 Mockito 建立和配置 mock 物件
除了上面所說的使用 mock() 靜態方法外,Mockito 還支援通過 @Mock
註解的方式來建立 mock 物件。
如果你使用註解,那麼必須要例項化 mock 物件。Mockito 在遇到使用註解的欄位的時候,會呼叫MockitoAnnotations.initMocks(this)
來初始化該 mock 物件。另外也可以通過使用@RunWith(MockitoJUnitRunner.class)
來達到相同的效果。
通過下面的例子我們可以瞭解到使用@Mock
的方法和MockitoRule
規則。
import static org.mockito.Mockito.*;
public class MockitoTest {
@Mock
MyDatabase databaseMock; (1)
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); (2)
@Test
public void testQuery() {
ClassToTest t = new ClassToTest(databaseMock); (3)
boolean check = t.query("* from t"); (4)
assertTrue(check); (5)
verify(databaseMock).query("* from t"); (6)
}
}
-
告訴 Mockito 模擬 databaseMock 例項
-
Mockito 通過 @mock 註解建立 mock 物件
-
使用已經建立的mock初始化這個類
-
在測試環境下,執行測試類中的程式碼
-
使用斷言確保呼叫的方法返回值為 true
-
驗證 query 方法是否被
MyDatabase
的 mock 物件呼叫
4.3. 配置 mock
當我們需要配置某個方法的返回值的時候,Mockito 提供了鏈式的 API 供我們方便的呼叫
when(….).thenReturn(….)
可以被用來定義當條件滿足時函式的返回值,如果你需要定義多個返回值,可以多次定義。當你多次呼叫函式的時候,Mockito 會根據你定義的先後順序來返回返回值。Mocks 還可以根據傳入引數的不同來定義不同的返回值。譬如說你的函式可以將anyString
或者 anyInt
作為輸入引數,然後定義其特定的放回值。
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
@Test
public void test1() {
// 建立 mock
MyClass test = Mockito.mock(MyClass.class);
// 自定義 getUniqueId() 的返回值
when(test.getUniqueId()).thenReturn(43);
// 在測試中使用mock物件
assertEquals(test.getUniqueId(), 43);
}
// 返回多個值
@Test
public void testMoreThanOneReturnValue() {
Iterator i= mock(Iterator.class);
when(i.next()).thenReturn("Mockito").thenReturn("rocks");
String result=i.next()+" "+i.next();
// 斷言
assertEquals("Mockito rocks", result);
}
// 如何根據輸入來返回值
@Test
public void testReturnValueDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo("Mockito")).thenReturn(1);
when(c.compareTo("Eclipse")).thenReturn(2);
// 斷言
assertEquals(1,c.compareTo("Mockito"));
}
// 如何讓返回值不依賴於輸入
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
// 斷言
assertEquals(-1 ,c.compareTo(9));
}
// 根據引數型別來返回值
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
// 斷言
Todo todo = new Todo(5);
assertEquals(todo ,c.compareTo(new Todo(1)));
}
對於無返回值的函式,我們可以使用doReturn(…).when(…).methodCall
來獲得類似的效果。例如我們想在呼叫某些無返回值函式的時候丟擲異常,那麼可以使用doThrow
方法。如下面程式碼片段所示
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
// 下面測試用例描述瞭如何使用doThrow()方法
@Test(expected=IOException.class)
public void testForIOException() {
// 建立並配置 mock 物件
OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();
// 使用 mock
OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
streamWriter.close();
}
4.4. 驗證 mock 物件方法是否被呼叫
Mockito 會跟蹤 mock 物件裡面所有的方法和變數。所以我們可以用來驗證函式在傳入特定引數的時候是否被呼叫。這種方式的測試稱行為測試,行為測試並不會檢查函式的返回值,而是檢查在傳入正確引數時候函式是否被呼叫。
import static org.mockito.Mockito.*;
@Test
public void testVerify() {
// 建立並配置 mock 物件
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// 呼叫mock物件裡面的方法並傳入引數為12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 檢視在傳入引數為12的時候方法是否被呼叫
verify(test).testing(Matchers.eq(12));
// 方法是否被呼叫兩次
verify(test, times(2)).getUniqueId();
// 其他用來驗證函式是否被呼叫的方法
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}
4.5. 使用 Spy 封裝 java 物件
@Spy或者spy()
方法可以被用來封裝 java 物件。被封裝後,除非特殊宣告(打樁 stub),否則都會真正的呼叫物件裡面的每一個方法
import static org.mockito.Mockito.*;
// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);
// 可用 doReturn() 來打樁
doReturn("foo").when(spy).get(0);
// 下面程式碼不生效
// 真正的方法會被呼叫
// 將會丟擲 IndexOutOfBoundsException 的異常,因為 List 為空
when(spy.get(0)).thenReturn("foo");
方法verifyNoMoreInteractions()
允許你檢查沒有其他的方法被呼叫了。
4.6. 使用 @InjectMocks 在 Mockito 中進行依賴注入
我們也可以使用@InjectMocks
註解來建立物件,它會根據型別來注入物件裡面的成員方法和變數。假定我們有 ArticleManager 類
public class ArticleManager {
private User user;
private ArticleDatabase database;
ArticleManager(User user) {
this.user = user;
}
void setDatabase(ArticleDatabase database) { }
}
這個類會被 Mockito 構造,而類的成員方法和變數都會被 mock 物件所代替,正如下面的程式碼片段所示:
@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {
@Mock ArticleCalculator calculator;
@Mock ArticleDatabase database;
@Most User user;
@Spy private UserProvider userProvider = new ConsumerUserProvider();
@InjectMocks private ArticleManager manager; (1)
@Test public void shouldDoSomething() {
// 假定 ArticleManager 有一個叫 initialize() 的方法被呼叫了
// 使用 ArticleListener 來呼叫 addListener 方法
manager.initialize();
// 驗證 addListener 方法被呼叫
verify(database).addListener(any(ArticleListener.class));
}
}
- 建立ArticleManager例項並注入Mock物件
4.7. 捕捉引數
ArgumentCaptor
類允許我們在verification期間訪問方法的引數。得到方法的引數後我們可以使用它進行測試。
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class MockitoTests {
@Rule public MockitoRule rule = MockitoJUnit.rule();
@Captor
private ArgumentCaptor> captor;
@Test
public final void shouldContainCertainListItem() {
List asList = Arrays.asList("someElement_test", "someElement");
final List mockedList = mock(List.class);
mockedList.addAll(asList);
verify(mockedList).addAll(captor.capture());
final List capturedArgument = captor.>getValue();
assertThat(capturedArgument, hasItem("someElement"));
}
}
4.8. Mockito的限制
Mockito當然也有一定的限制。而下面三種資料型別則不能夠被測試
-
final classes
-
anonymous classes
-
primitive types
5. 在Android中使用Mockito
在 Android 中的 Gradle 構建檔案中加入 Mockito 依賴後就可以直接使用 Mockito 了。若想使用 Android Instrumented tests 的話,還需要新增 dexmaker 和 dexmaker-mockito 依賴到 Gradle 的構建檔案中。(需要 Mockito 1.9.5版本以上)
dependencies {
testCompile 'junit:junit:4.12'
// Mockito unit test 的依賴
testCompile 'org.mockito:mockito-core:1.+'
// Mockito Android instrumentation tests 的依賴
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
}
6. 例項:使用Mockito寫一個Instrumented Unit Test
6.1. 建立一個測試的Android 應用
建立一個包名為com.vogella.android.testing.mockito.contextmock
的Android應用,新增一個靜態方法 ,方法裡面建立一個包含引數的Intent,如下程式碼所示:
public static Intent createQuery(Context context, String query, String value) {
// 簡單起見,重用MainActivity
Intent i = new Intent(context, MainActivity.class);
i.putExtra("QUERY", query);
i.putExtra("VALUE", value);
return i;
}
6.2. 在app/build.gradle檔案中新增Mockito依賴
dependencies {
// Mockito 和 JUnit 的依賴
// instrumentation unit tests on the JVM
androidTestCompile 'junit:junit:4.12'
androidTestCompile 'org.mockito:mockito-core:2.0.57-beta'
androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
// Mockito 和 JUnit 的依賴
// tests on the JVM
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.+'
}
6.3. 建立測試
使用 Mockito 建立一個單元測試來驗證在傳遞正確 extra data 的情況下,intent 是否被觸發。
因此我們需要使用 Mockito 來 mock 一個Context
物件,如下程式碼所示:
package com.vogella.android.testing.mockitocontextmock;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TextIntentCreation {
@Test
public void testIntentShouldBeCreated() {
Context context = Mockito.mock(Context.class);
Intent intent = MainActivity.createQuery(context, "query", "value");
assertNotNull(intent);
Bundle extras = intent.getExtras();
assertNotNull(extras);
assertEquals("query", extras.getString("QUERY"));
assertEquals("value", extras.getString("VALUE"));
}
}
7. 例項:使用 Mockito 建立一個 mock 物件
7.1. 目標
建立一個 Api,它可以被 Mockito 來模擬並做一些工作
7.2. 建立一個Twitter API 的例子
實現 TwitterClient
類,它內部使用到了 ITweet
的實現。但是ITweet
例項很難得到,譬如說他需要啟動一個很複雜的服務來得到。
public interface ITweet {
String getMessage();
}
public class TwitterClient {
public void sendTweet(ITweet tweet) {
String message = tweet.getMessage();
// send the message to Twitter
}
}
7.3. 模擬 ITweet 的例項
為了能夠不啟動複雜的服務來得到 ITweet
,我們可以使用 Mockito 來模擬得到該例項。
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
}
現在 TwitterClient
可以使用 ITweet
介面的實現,當呼叫 getMessage()
方法的時候將會列印
"Using Mockito is great" 資訊。
7.4. 驗證方法呼叫
確保 getMessage() 方法至少呼叫一次。
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
verify(iTweet, atLeastOnce()).getMessage();
}
7.5. 驗證
執行測試,檢視程式碼是否測試通過。
8. 模擬靜態方法
8.1. 使用 Powermock 來模擬靜態方法
因為 Mockito 不能夠 mock 靜態方法,因此我們可以使用 Powermock
。
import java.net.InetAddress;
import java.net.UnknownHostException;
public final class NetworkReader {
public static String getLocalHostname() {
String hostname = "";
try {
InetAddress addr = InetAddress.getLocalHost();
// Get hostname
hostname = addr.getHostName();
} catch ( UnknownHostException e ) {
}
return hostname;
}
}
我們模擬了 NetworkReader 的依賴,如下程式碼所示:
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {
// 測試程式碼
@Test
public void testSomething() {
mockStatic( NetworkUtil.class );
when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );
// 與 NetworkReader 協作的測試
}
8.2.用封裝的方法代替Powermock
有時候我們可以在靜態方法周圍包含非靜態的方法來達到和 Powermock 同樣的效果。
class FooWraper {
void someMethod() {
Foo.someStaticMethod()
}
}
9. Mockito 參考資料
from: https://github.com/xitu/gold-miner/blob/master/TODO/Unit-tests-with-Mockito.md