前言
健壮的代码需要良好的单元测试,单元测试需要模拟其它单元的功能,所以需要一个强大易用的mock框架。
需要了解的一些测试概念
深入理解Mockito(3)–Partial Mock(部分模拟)
基本要点:
-
状态检测和行为检测
- 状态检测:通过检查方法的返回值来判断方法是否运行成功
- 行为检测:方法运行之后,通过检测方法的执行行为(或者说执行顺序)进行判断方法是否运行成功
- 替换对象:stub(桩)和mock(模拟对象)。前者多用于状态测试,或者多用于行为测试。
- 部分模拟,模拟依赖对象的部分方法, 剩下的方法能够正常的运行其内部逻辑。
对抗软件复杂度的战争大家单元测试实践中的一些怪现象时,常常会感到匪夷所思,这些现象会包括:
- 低质量的单元测试:包括不写 assert,到处是 print 语句,要人去验证。
- 不稳定的单元测试:代码是好的,测试是失败的,测试集无法被信任。
- 耗时非常长的单元测试:运行一下要几十分钟或者几小时。
- 用代码生成单元测试:对不起,我认为这个东西除了提升覆盖率虚荣指标外,毫无意义。
mockitio
测试驱动的开发(Test Driven Design, TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,一个很普遍的问题是,要测试的类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。 所幸,我们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。
- 验证:是否调用了模拟类的方法
- 调用的时候传入参数,参数匹配
- 一个方法的执行结果,包括返回值和异常。这些mock 也可以模拟
源码分析
Mockito 通过 ByteBuddy(旧版本使用cglib) 来创建 mock 类并进行实例化 proxy 对象。本质上是一个Proxy模式的应用。
cglib使用
cglib(底层基于ASM) - Byte Code Generation Library is high level API to generate and transform Java byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access.
net.sf.cglib.proxy.Enhancer 类提供了非常简洁的API来创建代理对象,有两种回调的防方式:InvocationHandler和MethodInterceptor。mockito中使用了MethodInterceptor方式。
public class User {
public String hello(String name){
return "hello " + name;
}
}
生成代理类:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
if (method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
return "hello cglib!";
} else {
return proxy.invokeSuper(obj, args);
}
}
});
User proxy = (User) enhancer.create();
System.out.println(proxy.hello("abc"));
代理模式中,肯定会提供类似Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
的回调,用于我们扩充被代理类的执行逻辑。
mockito
本文为简化源码分析的复杂性,从mockito 1.0 版本源码入手。示例代码
List mock = Mockito.mock(List.class);
Mockito.stub(mock.add("test")).toReturn(true);
System.out.println(mock.add("test"));
mock方法的实质
public class Mockito extends Matchers {
public static <T> T mock(Class<T> classToMock) {
return MockUtil.createMock(classToMock, MOCKING_PROGRESS);
}
}
public class MockUtil {
public static <T> T createMock(Class<T> classToMock, MockingProgress progress) {
MockFactory<T> proxyFactory = new MockFactory<T>();
MockHandler<T> mockHandler = new MockHandler<T>(progress, new MatchersBinder());
MethodInterceptorFilter<MockHandler<T>> filter = new MethodInterceptorFilter<MockHandler<T>>(classToMock, mockHandler);
return proxyFactory.createMock(classToMock, filter);
}
}
MockHandler 的创建,揉和一些全局对象,同时还创建了stubber对象。
proxyFactory.createMock(classToMock, filter)
便是调用cglib 创建了代理对象。所以,代码的重点就转到了MockHandler(实现cglib MethodInterceptor 接口) 的实现上。
做桩及执行桩方法
-
第一次执行 mock.add 方法的实质。就是 MockHandler中来自MethodInterceptor接口的intercept 方法的执行。在
mock.add("test")
方法中- 可以知道方法名是add,参数是test
- 将这个数据封装为 invocationMatcher,存在stubber中
- 创建一个OngoingStubbing(包括toReturn和toThrow 方法) 关联stubber(通过内部类实现),由MockingProgress包裹, 保存在thread local中
- stub方法的实质。从threadlocal中取出OngoingStubbing并返回
- toRetun方法的实质。OngoingStubbing 拿到与之绑定的 stubber,将结果与 invocationMatcher 关联起来。
- 第二次执行 mock.add 方法的实质。从stubber中 取出 对应的 invocationMatcher 的 结果。
在整个做桩的过程中,有一个MockingProgress,第一次执行mock.add
时,标记做桩开始。OngoingStubbing.toReturn 或者 toThrow时,标记做桩结束。
注意,mock.业务方法
都是执行两次,一次用于做桩,有一次用于执行桩方法。verify 的逻辑类似,此处不再分析。
值得学习的细节
- 代码中使用 Invocation存储
mock.add("abc")
等信息,使用InvocationMatcher封装 Invocation 是为了从Invocation剥离匹配过程。因为mock.业务方法
都是执行两次,第二次执行时,能够根据Invocation 尽快的匹配 上一次存储的Invocation,尤其是 方法参数的匹配,比如Mockito.when(userDao.getUserById(1L)).thenReturn(new UserPO(1L,"user1",20));
时,uid=1时返回mock值。uid=2时就不返回了。此处装饰模式用的很精彩 -
MockingProgress 是一个接口,包括两个实现类MockingProgressImpl和ThreadSafeMockingProgress,MockingProgressImpl实现基本功能,而ThreadSafeMockingProgress在MockingProgressImpl 基础上实现线程安全的功能。
public class ThreadSafeMockingProgress implements MockingProgress { private static ThreadLocal<MockingProgress> mockingProgress = new ThreadLocal<MockingProgress>(); static MockingProgress threadSafely() { if (mockingProgress.get() == null) { mockingProgress.set(new MockingProgressImpl()); } return mockingProgress.get(); } }
MockingProgressImpl 不直接抛头露面,这活儿交给ThreadSafeMockingProgress,这个线程安全的技巧值得学习。
小结
mockito 源码分析汇总:
- 使用代理模式,构建mock类
- mock类的业务方法执行两次,第一次及toReturn等操作用于汇总方法的执行、返回值等信息,挂到thread local上。第二次执行,则是从threadlocal中获取 数据并返回。
一些使用建议
- 一般web开发是controller-service-dao,模拟service 测试Controller比较简单,mock service类的接口方法即可。但对于模拟dao 测试service,一般不使用mock,而是直接操作数据库,spring-test 支持 单元测试中事务自动回滚,清理测试数据。
- mockito 与 spring 整合 参见使用Mockito和SpringTest进行单元测试