Mockito 原理
# 介绍
Mockito 也是 Java 单元测试的老大哥了,在 2.x 也支持了 mockStatic,之前都是配合 PowerMock 进行 mockstatic 使用的。我们开发中也离不开单元测试,所以理解单元测试的原理更加便于我们写好 case 。
# 单元测试步骤
- 设定目标
- 设置消费条件
- 预期返回结果
- 消费并检验返回结果
# 原理介绍
# 代理
我们写 Java 应该都是知道代理模式的,有 JDK 动态代理与 Cglib 代理等,其实 Mock 也是基于代理实现的,在我们调用 mock(xxxx) 的时候就是对这个对象创建了一个代理,拦截每个方法,保存方法的入参,返回值等信息。在我们真正调用时,就是通过代理对象进行操作,方法都被代理了那方法想返回什么东西也是可以随意指定了。
# 打桩
打桩就是标识在哪个位置进行代理,比如我们经常写的
when(xxxMock.xxMethod()).thenReturn(xxx)
这其实就是打桩,在调用这个方法的时候,其实就是调用了我们的代理方法,所以我们知道入参信息,所以就可以把方法名、方法入参记录下来,生成一个匹配器(桩),如果下次调用的时候匹配了,就满足这个桩点,执行桩点设置的返回值(或者其他 answer)。
# 源码演示
# 调用 obj.get("a") 的时候返回 "b"
when(obj.get(eq("a"))).thenReturn("b")
2
当我们调用 when 时,内部会调用
# 记录开始打桩的位置
mockingProgress.stubbingStarted();
2
当我们调用 eq
等匹配逻辑的时候会记录参数
argumentMatcherStorage.reportMatcher(matcher)
然后当我们 then 的时候内部会调用
# 保存对应匹配模式的对应的应答方式然后标记完成
mockingProgress.stubbingCompleted(invocation);
2
然后当我们真正去调用方法的时候
obj.get("a")
就会去通过当前调用的方法,是否打桩过,并且存在匹配器能够匹配上,最终得到匹配器的应答器,得到返回值,直接返回。
# org.mockito.internal.handler.MockHandlerImpl#handle
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(mockingProgress.getArgumentMatcherStorage(),invocation);
invocationContainerImpl.findAnswerFor(invocation)
2
3
4
# 静态方法 mock
这个在 mockito 1.x 时是不支持的,还需要利用 powermock 实现,在 mockito 2.x 时,就支持了 mockStatic 了。那么静态方法我们改怎么 mock 呢。
PowerMock 的实现原理是通过 @PrepareForTest
注解指定出哪些是需要 mockStatic 的,然后在启动时通过一个 classLoader 加载这些类,并且通过动态修改字节码的方式进行 mock。
Mockito 实现原理也是字节码增强。
# 动手实现
Mock的实现关键是,实现动态代理,被 mock 的对象只是“假装”调用了该方法,然后返回假的值。
可以使用cglib来进行动态代理。通过class对象创建该对象的动态代理对象,然后设置该对象的父类与回调即可。并在回调函数中定义拦截器,实现自定义逻辑。
public class Mockito {
/**
* 根据class对象创建该对象的代理对象
* 1、设置父类;2、设置回调
* 本质:动态创建了一个class对象的子类
*
* @return
*/
public static <T> T mock(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(new MockInterceptor());
return (T)enhancer.create();
}
private static class MockInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return null;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 实现 stub
首先定义一个类,来表示对函数的调用,重写equals()方法,通过函数名 + 参数列表来判断调用是否相同。
public class Invocation {
private final Object mock;
private final Method method;
private final Object[] arguments;
private final MethodProxy proxy;
public Invocation(Object mock, Method method, Object[] args, MethodProxy proxy) {
this.mock = mock;
this.method = method;
this.arguments = copyArgs(args);
this.proxy = proxy;
}
private Object[] copyArgs(Object[] args) {
Object[] newArgs = new Object[args.length];
System.arraycopy(args, 0, newArgs, 0, args.length);
return newArgs;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !obj.getClass().equals(this.getClass())) { return false; }
Invocation other = (Invocation)obj;
return this.method.equals(other.method) && this.proxy.equals((other).proxy)
&& Arrays.deepEquals(arguments, other.arguments);
}
@Override
public int hashCode() {
return 1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
接下来,在 MockInterceptor 类中,需要做两个操作。
- 为了设置方法的返回值,需要存放对方法的引用(Invocation)
- 调用方法时,检查是否已经设置了该方法的返回值(results)。如果设置了,则返回该值。
public class Mockito {
private static Map<Invocation, Object> results = new HashMap<Invocation, Object>();
private static Invocation lastInvocation;
public static <T> T mock(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(new MockInterceptor());
return (T)enhancer.create();
}
private static class MockInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Invocation invocation = new Invocation(proxy, method, args, proxy);
lastInvocation = invocation;
if (results.containsKey(invocation)) {
return results.get(invocation);
}
return null;
}
}
public static <T> When<T> when(T o) {
return new When<T>();
}
public static class When<T> {
public void thenReturn(T retObj) {
results.put(lastInvocation, retObj);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 测试
@Test
public void test() {
Calculate calculate = mock(Calculate.class);
when(calculate.add(1, 1)).thenReturn(1);
Assert.assertEquals(1, calculate.add(1, 1));
}
2
3
4
5
6