作者|mghio
编辑|包包
如何实现一个微型的 Mock 框架
知道了 Mockito 的运行原理之后,接下来看看要如何自己去实现一个类似功能的 mock 框架出来,看到方法拦截这里我相信你已经知道了,其实这就是 AOP 啊,但是通过阅读其源码发现 Mockito 其实并没有使用我们熟悉的 Spring AOP 或者 AspectJ 做的方法拦截,而是通过运行时增强库 Byte Buddy 和反射工具库 Objenesis 生成和初始化 mock 对象的。
现在,通过以上分析和源码阅读可以定义出一个简单版本的 mock 框架了,将自定义的 mock 框架命名为 imock。这里有一点需要注意的是,Mockito 有一个好处是,它不需要进行初始化,可以直接通过其提供的静态方法来立即使用它。在这里我们也使用相同名称的静态方法,通过 Mockito 源码:
很容易看出 Mockito 类最终都是委托给 MockitoCore 去实现的功能,而其只提供了一些面向使用者易用的静态方法,在这里我们也定义一个这样的代理对象 IMockCore,这个类中需要一个创建 mock 对象的方法 mock 和一个给方法设定返回值的 thenReturn 方法,同时该类中持有一个方法调用详情 InvocationDetail 集合列表,这个类是用来记录方法调用详细信息的,然后 when 方法仅返回列表中的最后一个 InvocationDetail,这里列表可以直接使用 Java 中常用的 ArrayList 即可,这里的 ArrayList 集合列表就实现了 Mockito 中的 OngoingStubbing 的功能。
根据方法的三要素方法名、方法参数和方法返回值很容易就可以写出 InvocationDetail 类的代码,为了对方法在不同类有同名的情况区分,还需要加上类全称字段和重写该类的 equals 和 hashCode 方法(判断是否在调用方法集合列表时需要根据该方法判断),代码如下所示:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */ public class InvocationDetail {
private String attachedClassName;
private String methodName;
private Object[] arguments;
private T result;
public InvocationDetail(String attachedClassName, String methodName,
Object[] arguments) {
this.attachedClassName = attachedClassName;
this.methodName = methodName;
this.arguments = arguments;
}
public void thenReturn(T t) {
this.result = t;
}
public T getResult() {
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
InvocationDetail> behaviour = (InvocationDetail>) o;
return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
Objects.equals(methodName, behaviour.methodName) &&
Arrays.equals(arguments, behaviour.arguments);
}
@Override
public int hashCode() {
int result = Objects.hash(attachedClassName, methodName);
result = (31 * result) + Arrays.hashCode(arguments);
return result;
}
}
接下来就是如何去创建我们的 mock 对象了,在这里我们也使用 Byte Buddy 和 Objenesis 库来创建 mock 对象,IMockCreator 接口定义如下:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */public interface IMockCreator { T createMock(Class mockTargetClass, List behaviorList);}
实现类 ByteBuddyIMockCreator 使用 Byte Buddy 库在运行时动态生成 mock 类对象代码然后使用 Objenesis 去实例化该对象。代码如下:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */
public class ByteBuddyIMockCreator implements IMockCreator {
private final ObjenesisStd objenesisStd = new ObjenesisStd();
@Override
public T createMock(Class mockTargetClass,
List behaviorList) {
ByteBuddy byteBuddy = new ByteBuddy();
Class classWithInterceptor = byteBuddy.subclass(mockTargetClass)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(
InterceptorDelegate.class))
.defineField("interceptor",
IMockInterceptor.class, Modifier.PRIVATE)
.implement(IMockIntercepable.class)
.intercept(FieldAccessor.ofBeanProperty())
.make()
.load(getClass()
.getClassLoader(),
Default.WRAPPER).getLoaded();
T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(
behaviorList));
return mockTargetInstance;
}
}
基于以上分析我们可以很容易写出创建 mock 对象的 IMockCore 类的代码如下:
public class IMockCore {
private final List < InvocationDetail > invocationDetailList = new ArrayList < >(8);
private final IMockCreator mockCreator = new ByteBuddyIMockCreator();
public < T > T mock(Class < T > mockTargetClass) {
T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
return result;
}@SuppressWarnings("unchecked") public < T > InvocationDetail < T > when(T methodCall) {
int currentSize = invocationDetailList.size();
return (InvocationDetail < T > ) invocationDetailList.get(currentSize - 1);
}
}
提供给使用者的类 IMock 只是对 IMockCore 进行的简单调用而已,代码如下:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */
public class IMock {
private static final IMockCore IMOCK_CORE = new IMockCore();
public static < T > T mock(Class < T > clazz) {
return IMOCK_CORE.mock(clazz);
}
public static < T > InvocationDetail when(T methodCall) {
return IMOCK_CORE.when(methodCall);
}
}
通过以上步骤,我们就已经实现了一个微型的 mock 框架了,下面来个实际例子测试一下,首先创建一个 Target 对象:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */
public class Target {
public String foo(String name) {
return String.format("Hello, %s", name);
}
}
然后编写其对应的测试类 IMockTest 类如下:
/** * @author mghio * @date: 2020-05-30 * @version: 1.0 * @description: * @since JDK 1.8 */
public class IMockTest {@Test public void test_foo_method() {
String exceptedResult = "Mocked mghio";
Target mockTarget = IMock.mock(Target.class);
IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);
String actualResult = mockTarget.foo("mghio");
assertEquals(exceptedResult, actualResult);
}
}
以上测试的可以正常运行,达到了和 Mockito 测试框架一样的效果,运行结果如下:
上面只是列出了一些关键类的源码,自定义 IMock 框架的所有代码已上传至 Github 仓库 imock,感兴趣的朋友可以去看看。
本文只是介绍了 Mockito 的一些使用方法,这只是该框架提供的最基础功能,更多高级的用法可以去官网阅读相关的文档,然后介绍了框架中 when(...).thenReturn(...) 定义行为方法的实现方式并按照其源码思路实现了一个相同功能的简易版的 imock 。虽然进行单元测试有很多优点,但是也不可盲目的进行单元测试,在大部分情况下只要做好对项目中逻辑比较复杂、不容易理解的核心业务模块以及项目中公共依赖的模块的单元测试就可以了。