模擬測試框架之Mockito使用及原理分析
阿新 • • 發佈:2022-05-23
前言
當我們進行單元測試時,可能某個依賴的服務還沒有開發完成(如RPC或HTTP呼叫),這種情況下我們就可以對依賴服務建立一個模擬物件,這樣我們就可以更加關注於當前的測試類,而不是依賴的服務類。Mockito是一個強大的模擬測試框架,可以讓我們很方便的建立模擬物件並進行行為驗證。
新增maven依賴
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.3.3</version> </dependency>
建立模擬物件
import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.mockito.Mockito; public class TestMock2 { public static void main(String[] args) { List<String> mockList = Mockito.mock(List.class); System.out.println(mockList.getClass()); System.out.println(Arrays.toString(mockList.getClass().getInterfaces())); mockList = Mockito.mock(ArrayList.class); System.out.println(mockList.getClass()); System.out.println(Arrays.toString(mockList.getClass().getInterfaces())); } }
既可以對介面建立模擬物件,也可以對具體的實現類建立模擬物件,Mockito底層使用ByteBuddy庫來建立代理類,使用Objenesis庫來例項化物件。
ByteBuddy是一個程式碼生成和操作的類庫,類似於Cglib、javassist,底層也是ASM庫,官網。
Objenesis是一個小的java庫,可以讓我們繞過構造器來例項化物件,Spring通過Cglib建立代理物件的過程中就使用到了Objenesis,
更多資訊可以檢視java中Objenesis庫簡單使用。
建立部分模擬物件
import java.util.ArrayList; import java.util.List; import org.mockito.Mockito; public class TestMock4 { public static void main(String[] args) { List<String> mockList = Mockito.mock(ArrayList.class); List<String> spyList = Mockito.spy(ArrayList.class); mockList.add("hello"); spyList.add("hello"); System.out.println(mockList.get(0));//null System.out.println(spyList.get(0));//hello } }
spy()方法和mock()的區別在於
- mock()方法在沒有找到對應的配置行為時,返回預設結果,如int型別返回0
- spy()方法在沒有找到對應的配置行為時,委託給被代理物件處理,這裡就是ArrayList。如果被代理物件為介面,也是返回預設結果。
具體可以檢視CallsRealMethods類實現。
配置方法行為
import java.util.List;
import org.mockito.Mockito;
public class TestMock2 {
public static void main(String[] args) {
List<String> mockList = Mockito.mock(List.class);
Mockito.when(mockList.size()).thenReturn(1);
System.out.println(mockList.size());
Mockito.when(mockList.get(0)).thenReturn("hello");
System.out.println(mockList.get(0));
//丟擲異常
Mockito.when(mockList.get(0)).thenThrow(new RuntimeException("error"));
System.out.println(mockList.get(0));
}
}
為List.size()行為配置一個結果1,下次呼叫此方法時直接返回此結果。
沒有配置行為時,使用預設結果,配置在DefaultMockitoConfiguration的ReturnsEmptyValues類中,如對int值返回0,對Iterable型別,返回一個空的ArrayList。
接下來分析一下原理:
- 當我們呼叫List.size()時,Mockito會攔截此方法,將方法呼叫(List.size())的詳細資訊儲存到模擬物件的上下文中
- 呼叫Mockito.when()方法時,會從上下文(具體為MockingProgressImpl)中獲取到最後一次方法呼叫(List.size())資訊
- 將thenReturn()的引數作為結果儲存起來,下次呼叫時直接獲取。
方法呼叫容器為InvocationContainerImpl,其中包含一系列StubbedInvocationMatcher物件,每一個物件都是一個方法呼叫和具體結果的封裝。
驗證方法呼叫次數
import java.util.List;
import org.mockito.Mockito;
public class TestMock3 {
public static void main(String[] args) {
List<String> mockList = Mockito.mock(List.class);
mockList.size();
mockList.size();
Mockito.verify(mockList, Mockito.times(2)).size();
}
}
如果方法沒有被呼叫2次,就會丟擲異常。
總結
Mockito基本原理就是對介面或具體類建立動態代理物件,在實際進行方法呼叫時,會查詢是否已經配置了結果,沒有就使用預設結果。