什么是 Mockito
Mockito is a mocking framework, JAVA-based library that is used for effective unit testing of JAVA applications. Mockito is used to mock interfaces so that a dummy functionality can be added to a mock interface that can be used in unit testing.
Mockito 是一個模擬框架,可以有效地來進行 Java 單元測試。Mockito 可以用來模擬接口,使得在單元測試中可以使用一個虛構的方法。
為什么需要模擬?
單元測試的想法是我們要測試我們的代碼而不測試依賴。有時候我們不想依靠依賴,或者說依賴沒有準備好,此時我們需要模擬。
基本用法
-
mock()
/@Mock
: 創建模擬- optionally specify how it should behave via Answer/MockSettings
-
when()
/given()
來指定模擬的行為(方法) - 默認情況下,調用 mock 對象的帶返回值的方法會返回默認的值,比如返回
null
、0
值或者false
等。 - 相同的方法和參數唯一確認一個代理。比如你可以分別代理
get(int)
方法在參數分別為0
和1
時的不同行為。
spy()
/@Spy
: 實現部分模擬, 真正的方法會被調用,但是也可以被 stubbing 和 verify@InjectMocks
: 自動注入被@Spy
或@Mock
注解的屬性-
verify()
: 驗證方法是否被調用,調用了幾次- 可以使用靈活的匹配參數,例如
any()
- 也可以通過
@Captor
來捕獲參數
- 可以使用靈活的匹配參數,例如
具體參見:https://static.javadoc.io/org.mockito/mockito-core/2.22.0/org/mockito/Mockito.html
在這里通過 maven 進行構建:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.22.0</version>
<scope>test</scope>
</dependency>
</dependencies>
1. 使用 verify 來驗證行為,例如方法是否被調用
import org.junit.Test;
import java.util.List;
import static org.mockito.Mockito.*;
public class TestRunner {
@Test
public void Test1() {
// 模擬一個接口
List mockedList = mock(List.class);
// 使用模擬對象
mockedList.add("one");
mockedList.clear();
// 驗證行為,方法是否被調用
verify(mockedList).add("one");
verify(mockedList).clear();
}
}
2. 如何使用 stubbing 存根
@Test
public void Test2() {
// 不光可以模擬接口,可以模擬一個實體類
LinkedList mockedList = mock(LinkedList.class);
// stubbing 存根
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 打印 first
System.out.println(mockedList.get(0));
// 拋出 java.lang.RuntimeException
System.out.println(mockedList.get(1));
// 打印 "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
}
3. 參數匹配
例如我們可以使用 anyInt()
來匹配任意的整數類型。
更多的內嵌 matcher 和自定義 matcher,請參見:https://static.javadoc.io/org.mockito/mockito-core/2.22.0/org/mockito/ArgumentMatchers.html
@Test
public void Test3() {
// 不光可以模擬接口,可以模擬一個實體類
LinkedList mockedList = mock(LinkedList.class);
// stubbing 存根,使用內嵌的 anyInt() 來匹配參數
when(mockedList.get(anyInt())).thenReturn("element");
// 打印 element
System.out.println(mockedList.get(999));
// 驗證行為,方法是否被調用
verify(mockedList).get(anyInt());
}
4. 驗證方法被調用的次數
@Test
public void Test4() {
// 不光可以模擬接口,可以模擬一個實體類
LinkedList mockedList = mock(LinkedList.class);
// 使用 mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// 驗證方法被調用過多少次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// 驗證方法沒有被調用過
verify(mockedList, never()).add("never happened");
// 驗證方法被調用過多少次
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
}
5. 驗證方法的調用順序
@Test
public void Test5() {
List singleMock = mock(List.class);
// 使用 mock
singleMock.add("was added first");
singleMock.add("was added second");
// 創建 InOrder
InOrder inOrder = inOrder(singleMock);
// 驗證先調用 "was added first",再調用 "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
}
6. 使用 @Mock 注解
- Minimizes repetitive mock creation code. 簡化 Mock 的創建
- Makes the test class more readable. 增加代碼的可讀性
- Makes the verification error easier to read because the field name is used to identify the mock.
@Mock List mockedList;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void Test6() {
// 使用模擬對象
mockedList.add("one");
mockedList.clear();
// 驗證行為,方法是否被調用
verify(mockedList).add("one");
verify(mockedList).clear();
}
7. 使用 stubbing 存根模擬連續的調用
@Test
public void Test7() {
// 模擬一個接口
List mockedList = mock(List.class);
when(mockedList.get(anyInt()))
.thenThrow(new RuntimeException())
.thenReturn("foo");
// 第一次調用,拋出異常
mockedList.get(1);
// 第二次調用,打印 foo
System.out.println(mockedList.get(1));
}
也可以這樣使用:
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
8. 使用帶有 callback 回調的 stubbing 存根
@Test
public void Test8() {
// 模擬一個接口
List mockedList = mock(List.class);
when(mockedList.get(anyInt())).thenAnswer(
new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + Arrays.toString(args);
}
});
// 打印 "called with arguments: [1]"
System.out.println(mockedList.get(1));
}
9. 使用 doReturn(),doThrow(),doAnswer(),doNothing(),doCallRealMethod() 來 stub 空方法 void method
@Test
public void Test9() {
// 模擬一個接口
List mockedList = mock(List.class);
doThrow(new RuntimeException()).when(mockedList).clear();
// 拋出異常 RuntimeException:
mockedList.clear();
}
10. 在真正的對象上 spy
When you use the spy then the real methods are called (unless a method was stubbed).
當你使用 spy 的時候,真正的對象上的方法會被調用,除非你使用了 stubbing,例如 when()...
@Test
public void Test10() {
List list = new LinkedList();
List spy = spy(list);
//using the spy calls *real* methods
spy.add("one");
spy.add("two");
// 打印 one
System.out.println(spy.get(0));
// 打印 2
System.out.println(spy.size());
verify(spy).add("one");
verify(spy).add("two");
}
11. 實現局部模擬
@Test
public void Test11() {
// 模擬一個接口
List mockedList = mock(LinkedList.class);
// 調用實際的方法,實現局部模擬
when(mockedList.size()).thenCallRealMethod();
System.out.println(mockedList.size());
}
12. 重置 Mock
通過 reset(mock);
方法,來重置之前設置的 stubbing。
示例
假設我們要測試一個計算器程序 CalculatorApplication
,但是該程序依賴于 CalculatorService
實現具體的計算過程。
代碼如下:
public interface CalculatorService {
public double add(double input1, double input2);
public double subtract(double input1, double input2);
public double multiply(double input1, double input2);
public double divide(double input1, double input2);
}
public class CalculatorApplication {
private CalculatorService calcService;
public void setCalculatorService(CalculatorService calcService) {
this.calcService = calcService;
}
public double add(double input1, double input2) {
return calcService.add(input1, input2);
}
public double subtract(double input1, double input2) {
return calcService.subtract(input1, input2);
}
public double multiply(double input1, double input2) {
return calcService.multiply(input1, input2);
}
public double divide(double input1, double input2) {
return calcService.divide(input1, input2);
}
}
問題來了:在測試時,我們可能并沒有 CalculatorService
這個接口的具體實現類,例如 CalculatorServiceImpl
。
因此我們需要在測試時模擬 CalculatorService
這個接口的行為。
此時我們使用 mockito 來模擬行為。
mockito 可以通過注解的方式來使用:
-
@RunWith(MockitoJUnitRunner.class)
:指定 Test Runner -
@InjectMocks
:Mark a field on which injection should be performed. 標識一個變量,該變量會被注入一個 Mock。例如CalculatorApplication
會被注入一個CalculatorService
的實現。- 注意:
CalculatorApplication
中需要定義一個set
方法來注入。
- 注意:
-
@Mock
:Mark a field as a mock. 標識一個變量,該變量會被 Mock。例如CalculatorService
。- 在標記出 Mock 后,可以通過
when
來模擬該 Mock 的行為。
- 在標記出 Mock 后,可以通過
示例如下:
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class Mockito_Test {
@InjectMocks
CalculatorApplication calculatorApplication = new CalculatorApplication();
@Mock
CalculatorService calcService;
@Test
public void testAdd() {
// 模擬 CalculatorService 的行為
when(calcService.add(10.0, 20.0)).thenReturn(30.00);
// 測試
Assert.assertEquals(calculatorApplication.add(10.0, 20.0), 30.0, 0);
}
}
Mockito 原理
首先我們要知道,Mock 對象這件事情,本質上是一個 Proxy 模式的應用。
Proxy 模式說的是,在一個真實對象前面,提供一個 Proxy 對象,所有對真實對象的調用,都先經過 Proxy 對象,然后由 Proxy 對象根據情況,決定相應的處理,它可以直接做一個自己的處理,也可以再調用真實對象對應的方法
Proxy 對象對調用者來說,可以是透明的,也可以是不透明的。
Mockito 就是用 Java 提供的 Dynamic Proxy API 來實現的。
關于 Java 的動態代理,請參見 Java 動態代理
Mockito 本質上就是在代理對象調用方法前,用 Stubbing 的方式設置其返回值,然后在真實調用時,用代理對象返回預設的返回值。
我們來看如下的代碼:
List mockedList = mock(List.class);
// 設置 mock 對象的行為 - 當調用其 get 方法獲取第 0 個元素時,返回 "first"
when(mockedList.get(0)).thenReturn("first");
Java 中的程序調用是以棧的形式實現的,對于 when()
方法,mockedList.get(0)
方法的調用對它是不可見的。when()
能接收到的,只有 mockedList.get(0)
的返回值。
所以,上面的代碼也等價于:
// stubbing 存根
Object ret = mockedList.get(0);
when(ret).thenReturn("first");
看看 when()
方法的源碼:
public <T> OngoingStubbing<T> when(T methodCall) {
MockingProgress mockingProgress = ThreadSafeMockingProgress.mockingProgress();
mockingProgress.stubbingStarted();
OngoingStubbing<T> stubbing = mockingProgress.pullOngoingStubbing();
if (stubbing == null) {
mockingProgress.reset();
throw Reporter.missingMethodInvocation();
} else {
return stubbing;
}
}
看看 OngoingStubbing
接口里有哪些方法:
public interface OngoingStubbing<T> {
OngoingStubbing<T> thenReturn(T var1);
OngoingStubbing<T> thenReturn(T var1, T... var2);
OngoingStubbing<T> thenThrow(Throwable... var1);
OngoingStubbing<T> thenThrow(Class<? extends Throwable> var1);
OngoingStubbing<T> thenThrow(Class<? extends Throwable> var1, Class... var2);
OngoingStubbing<T> thenCallRealMethod();
OngoingStubbing<T> thenAnswer(Answer<?> var1);
OngoingStubbing<T> then(Answer<?> var1);
<M> M getMock();
}
mock 對象所有的方法最終都會交由 MockHandlerImpl
的 handle
方法處理,部分代碼如下:
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl(this.invocationContainer);
ThreadSafeMockingProgress.mockingProgress().reportOngoingStubbing(ongoingStubbing);
StubbedInvocationMatcher stubbing = this.invocationContainer.findAnswerFor(invocation);
StubbingLookupNotifier.notifyStubbedAnswerLookup(invocation, stubbing, this.invocationContainer.getStubbingsAscending(), (CreationSettings)this.mockSettings);
Object ret;
if (stubbing != null) {
stubbing.captureArgumentsFrom(invocation);
try {
ret = stubbing.answer(invocation);
} finally {
ThreadSafeMockingProgress.mockingProgress().reportOngoingStubbing(ongoingStubbing);
}
return ret;
} else {
ret = this.mockSettings.getDefaultAnswer().answer(invocation);
DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
this.invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
return ret;
}
when
調用的基本形式是 when(mock.doSome())
,此時,當 mock.doSome()
時即會觸發上面的語句,OngoingStubbingImpl
表示正在對一個方法打樁的包裝,invocationContainerImpl
相當于一個 mock 對象的管家,記錄著 mock 對象方法的調用。