什么是 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 对象方法的调用。