標籤:
Mock 是什麼
mock 測試就是在測試過程中,對於某些不容易構造或者不容易擷取的對象,用一個虛擬對象來建立以便測試的測試方法。這個虛擬對象就是mock對象。mock對象就是真實對象在調試期間的代替品。
簡單的看一張圖
我們在測試類別 A 時,類 A 需要調用類 B 和類 C,而類 B 和類 C 又需要調用其他類如 D、E、F 等,假如類 D、E、F 構造很耗時又或者調用很耗時的話是非常不便於測試的(比如是 DAO 類,每次訪問資料庫都很耗時)。所以我們引入 Mock 對象。
如,我們將類 B 和類 C 替換成 Mock 對象,在調用類 B 和類 C 的方法時,用 Mock 對象的方法來替換(當然我們要自己設定參數和期望結果)而不用實際去調用其他類。這樣測試效率會高很多。
一句話為什麼要 Mock:因為我們實際編寫程式都不會是一個簡單類,而是有著複雜依賴關係的類,Mock 對象讓我們在不依賴具體對象的情況下完成測試。
Mock 的關鍵點Mock 對象
類比對象的概念就是我們想要建立一個可以替代實際對象的對象,這個類比對象要可以通過特定參數調用特定的方法,並且能返回預期結果。
Stub(樁)
樁指的是用來替換具體功能的程式段。樁程式可以用來類比已有程式的行為或是對未完成開發程式的一種臨時替代。
比如我們有一個擷取溫度的函數。
public double getTemperature(String Position) { double ret = TemperatureRead(Position);}
但是 TemperatureRead 函數要調用具體的硬體裝置,而硬體裝置沒準備好。
我們可以用 Stub 來替換
public double TemperatureRead(String position) { return 28;}
Stub:For replacing a method with code that returns a specified result
Mock:A stub with an expectations that the method gets called
設定預期
通過設定預期明確 Mock 對象執行時會發生什麼,比如返回特定的值、拋出一個異常、觸發一個事件等,又或者調用一定的次數。
驗證預期的結果
設定預期和驗證預期是同時進行的。設定預期在調用測試類別的函數之前完成,驗證預期則在它之後。所以,首先你設定好預期結果,然後去驗證你的預期結果是否正確。
Mock 的好處是什麼提前建立測試; TDD(測試驅動開發)
這是個最大的好處吧。如果你建立了一個Mock那麼你就可以在service介面建立之前寫Service Tests了,這樣你就能在開發過程中把測試添加到你的自動化測試環境中了。換句話說,類比使你能夠使用測試驅動開發。
團隊可以並行工作
這類似於上面的那點;為不存在的代碼建立測試。但前面講的是開發人員編寫測試程式,這裡說的是測試團隊來建立。當還沒有任何東西要測的時候測試團隊如何來建立測試呢?類比並針對類比測試!這意味著當service借口需要測試時,實際上QA團隊已經有了一套完整的測試組件;沒有出現一個團隊等待另一個團隊完成的情況。這使得類比的效益型尤為突出了。
你可以建立一個驗證或者示範程式
由於Mocks非常高效,Mocks可以用來建立一個概念證明,作為一個,或者作為一個你正考慮構建項目的示範程式。這為你決定項目接下來是否要進行提供了有力的基礎,但最重要的還是提供了實際的設計決策。
為無法訪問的資源編寫測試
這個好處不屬於實際效益的一種,而是作為一個必要時的“救生圈”。有沒有遇到這樣的情況?當你想要測試一個service介面,但service需要經過防火牆訪問,防火牆不能為你開啟或者你需要認證才能訪問。遇到這樣情況時,你可以在你能訪問的地方使用MockService替代,這就是一個“救生圈”功能。
Mock 可以交給使用者
在有些情況下,某種原因你需要允許一些外部來源訪問你的測試系統,像夥伴或者客戶。這些原因導致別人也可以訪問你的敏感資訊,而你或許只是想允許訪問部分測試環境。在這種情況下,如何向夥伴或者客戶提供一個測試系統來開發或者做測試呢?最簡單的就是提供一個mock,無論是來自於你的網路或者客戶的網路。soapUI mock非常容易配置,他可以運行在soapUI或者作為一個war包發布到你的java伺服器裡面。
隔離系統
有時,你希望在沒有系統其他部分的影響下測試系統單獨的一部分。由於其他系統部分會給測試資料造成幹擾,影響根據資料收集得到的測試結論。使用mock你可以移除掉除了需要測試部分的系統依賴的類比。當隔離這些mocks後,mocks就變得非常簡單可靠,快速可預見。這為你提供了一個移除了隨機行為,有重複模式並且可以監控特殊系統的測試環境。
Mockito介紹
Mockito 是一個簡單的流行的 Mock 架構。它能夠幫我們建立 Mock 對象,保持單元測試的獨立性。
使用它只需要在 Maven 中添加依賴即可。
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>2.0.2-beta</version> </dependency>
建立 Mock 對象通過方法建立
class CreateMock { @Before public void setup() { mockUserDao = mock(UserDao.class); userService = new UserServiceImpl(); userService.setUserDao(mockUserDao); }}
通過註解建立
class CreateMock { @Mock UserDao mockUserDao; @InjectMocks private UserServiceImpl userService; @Before public void setUp() { //初始化對象的註解 MockitoAnnotations.initMocks(this); } }
一個簡單的例子:
package Mockito;import org.junit.Assert;import org.junit.Test;import java.util.List;import static org.mockito.Mockito.*;public class MyTest { @Test public void myTest() { /* 建立 Mock 對象 */ List list = mock(List.class); /* 設定預期,當調用 get(0) 方法時返回 "111" */ when(list.get(0)).thenReturn("111"); Assert.assertEquals("asd", 1, 1); /* 設定後返回期望的結果 */ System.out.println(list.get(0)); /* 沒有設定則返回 null */ System.out.println(list.get(1)); /* 對 Mock 對象設定無效 */ list.add("12"); list.add("123"); /* 返回之前設定的結果 */ System.out.println(list.get(0)); /* 返回 null */ System.out.println(list.get(1)); /* size 大小為 0 */ System.out.println(list.size()); /* 驗證操作,驗證 get(0) 調用了 2 次 */ verify(list, times(2)).get(0); /* 驗證返回結果 */ String ret = (String)list.get(0); Assert.assertEquals(ret, "111"); }}
從上面代碼能看出一般使用的步驟:
設定方法預期返回
通過 when(list.get(0)).thenReturn(“111”); 來設定當調用 list.get(0) 時返回 “111”,該方法就是 Stub,替換我們實際的操作。
驗證方法調用
該方法驗證 get(0) 方法調用的次數 verify(list, times(2)).get(0);,還可以設定是否調用過,調用時間等等。
驗證傳回值
Assert.assertEquals(ret, “111”); 方法驗證 Mock 對象方法調用後的傳回值是否達到預期。
Mockito 使用設定 Mock 對象期望和傳回值
/* 表示第一次調用 someMethod() 返回 value1 第二次調用返回 value2 */when(mock.someMethod()).thenReturn(value1).thenReturn(value2); when(mock.someMethod()).thenReturn(value1, value2); /* 也可以設定兩次 */when(mock.someMethod()).thenReturn(value1);when(mock.someMethod()).thenReturn(value2);
另外一種寫法 doReturn()
/* 表示第一次調用 someMethod() 返回 value1 第二次調用返回 value2 */doReturn(value1).doReturn(value2).when(mock).someMethod(); /* 若返回 void,則設定為 doNothing() */doNothing().when(mock).someMethod();
對方法設定返回異常
/* 當調用 someMethod() 方法時會拋出異常 */when(mock.someMethod()).thenThrow(new RuntimeException());/* 對 void 方法設定 */ doThrow(new RuntimeException()).when(mock).someMethod();
參數匹配器
我們不一定要固定 Stub 調用時的參數,如 get(0)。可以通過參數匹配器來調用。
when(list.get(anyInt())).thenReturn("hello");
Mock 對象的行為驗證
package Mockito;import org.mockito.ArgumentCaptor;import org.mockito.InOrder;import org.testng.annotations.Test;import java.util.List;import static org.mockito.Mockito.*;/** * Created by andy.wwh on 2016/7/18. */public class Behavior { @Test public void behaviorCheck() { List mock1 = mock(List.class); List mock2 = mock(List.class); /* 設定預期 */ when(mock1.get(0)).thenReturn("hello world"); when(mock1.get(1)).thenReturn("hello world"); when(mock2.get(0)).thenReturn("hello world"); mock1.get(0); /* 驗證方法調用一次 */ verify(mock1).get(0); mock1.get(0); /* 驗證方法調用兩次 */ verify(mock1, times(2)).get(0); /* 驗證方法從未被調用過 */ verify(mock2, never()).get(0); /* 驗證方法 100 毫秒內調用兩次 */ verify(mock1, timeout(100).times(2)).get(anyInt()); /* 設定方法調用順序 */ InOrder inOrder = inOrder(mock1, mock2); inOrder.verify(mock1, times(2)).get(0); inOrder.verify(mock2, never()).get(1); /* 查詢是否存在被調用,但未被 verify 驗證的方法 */ verifyNoMoreInteractions(mock1, mock2); /* 驗證 Mock 對象是否沒有交發生 */ verifyZeroInteractions(mock1, mock2); /* 參數捕獲器 */ ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class); verify(mock1, times(2)).get(argumentCaptor.capture()); System.out.println("argument:" + argumentCaptor.getValue()); }}
驗證調用次數
verify(mock1, timeout(100).times(2)).get(anyInt());
除了代碼中的方法,Mockito 還提供了
- never() 沒有被調用,相當於times(0)
- atLeast(N) 至少被調用N次
- atLeastOnce() 相當於atLeast(1)
- atMost(N) 最多被調用N次
逾時驗證
通過 timeout 我們可以進行驗證程式執行時間是否符合規則。
方法調用順序
Inorder 可以驗證方法調用的順序
verifyNoMoreInteractions 和 verifyZeroInteractions
verifyNoMoreInteractions:查詢是否存在被調用,但未被 verify 驗證的方法
verifyZeroInteractions:verifyZeroInteractions
ArgumentCaptor 參數捕獲器
可在驗證時對方法的參數進行捕獲,最後驗證捕獲的參數值。如果方法有多個參數都要捕獲驗證,那就需要建立多個ArgumentCaptor對象處理。
Spy 對象驗證
Mock 操作的全是虛擬對象。即使我們設定了 when(list.get(0)).thenReturn(1),我們調用如 size() 方法返回的還是 0。Mockito 還給我們提供了一種對真實對象操作的方法——Spy
做一個簡單的比較:
package Mockito;import org.testng.annotations.Test;import java.util.List;import static org.mockito.Mockito.mock;import static org.mockito.Mockito.when;/** * Created by andy.wwh on 2016/7/18. */public class MockObject { @Test public void MockTest() { List list = mock(List.class); when(list.get(0)).thenReturn("hello world"); System.out.println(list.get(0)); System.out.println(list.size()); }}
package Mockito;import org.testng.annotations.Test;import java.util.LinkedList;import java.util.List;import static org.mockito.Mockito.spy;import static org.mockito.Mockito.when;/** * Created by andy.wwh on 2016/7/18. */public class MockObject { @Test public void MockTest() { /* 建立真實對象 */ List list = new LinkedList(); List spy = spy(list); spy.add("hello"); when(spy.get(0)).thenReturn("hello world"); System.out.println(spy.get(0)); }}
看個官網的例子:
@Test public void spyTest() { List list = new LinkedList(); List spy = spy(list); // optionally, you can stub out some methods: when(spy.size()).thenReturn(100); // using the spy calls real methods spy.add("one"); spy.add("two"); // prints "one" - the first element of a list System.out.println(spy.get(0)); // size() method was stubbed - 100 is printed System.out.println(spy.size()); // optionally, you can verify verify(spy).add("one"); verify(spy).add("two"); }
參考:
Mockito 初探
Mockito:一個強大的用於 Java 開發的類比測試架構
Mock 類比測試簡介及 Mockito 使用入門