前言
Mockito是一款Java主流的单元测试框架,上手简单,mockito官网:http://mockito.org,API文档:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html,项目源码:https://github.com/mockito/mockito。
在我们写单元测试时,经常遇到待测试类依赖于多个类,从而形成一个大的依赖树,要在单元测试中完整的构建这一依赖是一件非常困难的事情。Mockito通过mock
方法和stub
方法,可以构建一个mock对象,然后对mock对象的行为进行打桩,让我们把单元测试聚焦于待测试类上。
然而,Mockito是如何实现这一功能的呢?本文就when
方法和stub
方法对其进行介绍。
单测环境
Maven包的引入:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
</dependency>
被测试类A:
public class A {
public String aMethod(){
return "hello world!";
}
}
单侧类:
public class ATest {
@Test
public void testA(){
A mock = Mockito.mock(A.class);
Mockito.when(mock.aMethod()).thenReturn("ABC");
}
}
mock对象的生成
mock对象的基本原理是生成一个被mock对象的代理类,然后对该类的方法进行拦截、代理增强。
mock对象的核心代码如下所示:
// org.mockito.internal.util.MockUtil
public static <T> T createMock(MockCreationSettings<T> settings) {
MockHandler mockHandler = createMockHandler(settings);
T mock = mockMaker.createMock(settings, mockHandler);
Object spiedInstance = settings.getSpiedInstance();
if (spiedInstance != null) {
new LenientCopyTool().copyToMock(spiedInstance, mock);
}
return mock;
}
总结起来流程如下:
Mockito中mock对象的生成使用ByteBuddy技术。
我们以Object类为例,通过重写它的toString方法,用来返回“Hello World!”来讲解Byte Buddy的使用方法。
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
比如我们想看下上述示例中生成的动态类到底长什么样子,可以调用方法 net.bytebuddy.dynamic.DynamicType#saveIn
来将生成的class保存成class文件(可在debug时再watcher中调用saveIn方法),然后在IDEA中进行查看,如下所示:
public class Object$ByteBuddy$F6rbnunr {
public String toString() {
return "Hello World!";
}
public Object$ByteBuddy$F6rbnunr() {
}
}
在Mockito中,动态生成代理类class对应的方法如下
// org.mockito.internal.creation.bytebuddy.SubclassBytecodeGenerator
public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
// ...
// Byte Buddy动态生成类
DynamicType.Builder<T> builder = byteBuddy.subclass(features.mockedType)
.name(name)
.ignoreAlso(isGroovyMethod())
.annotateType(features.stripAnnotations
? new Annotation[0]
: features.mockedType.getAnnotations())
.implement(new ArrayList<Type>(features.interfaces))
.method(matcher)
.intercept(dispatcher)
.transform(withModifiers(SynchronizationState.PLAIN))
.attribute(features.stripAnnotations
? MethodAttributeAppender.NoOp.INSTANCE
: INCLUDING_RECEIVER)
.method(isHashCode())
.intercept(hashCode)
.method(isEquals())
.intercept(equals)
.serialVersionUid(42L)
.defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
.implement(MockAccess.class)
.intercept(FieldAccessor.ofBeanProperty());
// ...
return builder.make()
.load(classLoader, loader.resolveStrategy(features.mockedType, classLoader, localMock))
.getLoaded();
}
这个方法看上去比较复杂,其实和bytebuddy示例代码原理一样,其最终会生成A的代理类如下
public class A$MockitoMock$883113934 extends A implements MockAccess {
private static final long serialVersionUID = 42L;
private MockMethodInterceptor mockitoInterceptor;
public boolean equals(Object var1) {
return ForEquals.doIdentityEquals(this, var1);
}
public String toString() {
return (String)DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$9GiAqxT8$4cscpe1, new Object[0], new A$MockitoMock$883113934$auxiliary$kDQsaeNa(this));
}
public int hashCode() {
return ForHashCode.doIdentityHashCode(this);
}
protected Object clone() throws CloneNotSupportedException {
return DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$9GiAqxT8$7m9oaq0, new Object[0], new A$MockitoMock$883113934$auxiliary$RfEBiKk5(this));
}
public String aMethod() {
return (String)DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$9GiAqxT8$cqor5a0, new Object[0], new A$MockitoMock$883113934$auxiliary$Av33OzJ6(this));
}
public void setMockitoInterceptor(MockMethodInterceptor var1) {
this.mockitoInterceptor = var1;
}
public MockMethodInterceptor getMockitoInterceptor() {
return this.mockitoInterceptor;
}
public A$MockitoMock$883113934() {
}
}
可以看到aMethod
方法被改写成了调用方法分发器DispatcherDefaultingToRealMethod
,我们看下它的interceptSuperCallable
方法
// DispatcherDefaultingToRealMethod
@RuntimeType
@BindingPriority(BindingPriority.DEFAULT * 2)
public static Object interceptSuperCallable(@This Object mock,
@FieldValue("mockitoInterceptor") MockMethodInterceptor interceptor,
@Origin Method invokedMethod,
@AllArguments Object[] arguments,
@SuperCall(serializableProxy = true) Callable<?> superCall) throws Throwable {
if (interceptor == null) {
return superCall.call();
}
return interceptor.doIntercept(
mock,
invokedMethod,
arguments,
new RealMethod.FromCallable(superCall)
);
}
可见其主要是调用了interceptor.doIntercept
方法,interceptor是mockA中的MockMethodInterceptor对象。MockMethodInterceptor对象中持有MockHandler对象,而MockMethodInterceptor#doIntercept
实际上就是调用了MockHandler#handle
。简言之,被mock对象的公有方法都被MockHandler类拦截了(其实是被MockMethodInterceptor
拦截,然后交由MockHandler
处理)。
此外,在mock对象生成后,也会生成MockingProgress
对象,该对象被保存在ThreadLocal中
// org.mockito.internal.MockCore
public <T> T mock(Class<T> typeToMock, MockSettings settings) {
...
T mock = createMock(creationSettings);
mockingProgress().mockingStarted(mock, creationSettings);
return mock;
}
该对象用于存储在mockito代码执行过程中各种信息,将在stub过程中起到很重要的作用。
对象如何打桩stub
一种最常见的打桩语句如下所示
// mock为A类的mock对象
Mockito.when(mock.aMethod()).thenReturn("ABC");
以上语句可以分为三步:
mock.aMethod()
方法调用Mockito.when()
静态方法调用.thenReturn()
打桩
打桩前的方法调用
在上文中我们已经知道,mock对象的方法被MockMethodInterceptor
所拦截,然后调用了MockHandler#handle
方法。
MockHandler结构如下图所示
其中
- Invocation:用来描述一次方法的调用
- ArgumentMatcher:用来描述方法调用中参数的匹配方法
- Answer:用来描述打桩行为,thenReturn即一种Answer
// org.mockito.internal.handler.MockHanlderImpl#handle
public Object handle(Invocation invocation) throws Throwable {
// 这个分支是处理doAnswer(xx).when(mock).methodXX()的打桩方式
// 这是一种给void返回类型方法打桩的方式
// 1. 执行doAnswer后会返回一个Stubber对象,其中持有List<Answer>
// 2. 执行.when方法后,会将List<Answer>拷贝到doAnswerStyleStubbing中
if (invocationContainer.hasAnswersForStubbing()) {
// stubbing voids with doThrow() or doAnswer() style
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(),
invocation
);
invocationContainer.setMethodForStubbing(invocationMatcher);
return null;
}
VerificationMode verificationMode = mockingProgress().pullVerificationMode();
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(),
invocation
);
mockingProgress().validateState();
// if verificationMode is not null then someone is doing verify()
if (verificationMode != null) {
// We need to check if verification was started on the correct mock
// - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
VerificationDataImpl data = new VerificationDataImpl(invocationContainer, invocationMatcher);
verificationMode.verify(data);
return null;
} else {
// this means there is an invocation on a different mock. Re-adding verification mode
// - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
mockingProgress().verificationStarted(verificationMode);
}
}
// prepare invocation for stubbing
// 在when中方法被第一次调用,会进入到这个逻辑
// 将本次调用放入到invocationForStubbing中, 并添添加到registeredInvocations中
// 生成OngoingStubbing对象,并将其存放到mockingProgess中
invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
mockingProgress().reportOngoingStubbing(ongoingStubbing);
// look for existing answer for this invocation
// 如果已经打好桩,则会在此处匹配到stub
StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
// TODO #793 - when completed, we should be able to get rid of the casting below
notifyStubbedAnswerLookup(invocation, stubbing, invocationContainer.getStubbingsAscending(),
(CreationSettings) mockSettings);
if (stubbing != null) {
stubbing.captureArgumentsFrom(invocation);
// 执行打桩的逻辑
try {
return stubbing.answer(invocation);
} finally {
//Needed so that we correctly isolate stubbings in some scenarios
//see MockitoStubbedCallInAnswerTest or issue #1279
mockingProgress().reportOngoingStubbing(ongoingStubbing);
}
} else {
Object ret = mockSettings.getDefaultAnswer().answer(invocation);
DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
//Mockito uses it to redo setting invocation for potential stubbing in case of partial mocks / spies.
//Without it, the real method inside 'when' might have delegated to other self method
//and overwrite the intended stubbed method with a different one.
//This means we would be stubbing a wrong method.
//Typically this would led to runtime exception that validates return type with stubbed method signature.
invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
return ret;
}
}
Mockito.when静态方法
// org.mockito.internal.MockitoCore
public <T> OngoingStubbing<T> when(T methodCall) {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
@SuppressWarnings("unchecked")
OngoingStubbing<T> stubbing = (OngoingStubbing<T>) mockingProgress.pullOngoingStubbing();
if (stubbing == null) {
mockingProgress.reset();
throw missingMethodInvocation();
}
return stubbing;
}
可见该方法即从mockingProgress
中拿到mock方法第一次调用时放入其中的OngoingStubbing
对象,然后返回。
OngoingStubbing#thenReturn打桩
// org.mockito.internal.stubbing.BaseStubbing
public OngoingStubbing<T> thenReturn(T value, T... values) {
// 会调用OngoingStubbingImpl#thenAnswer
OngoingStubbing<T> stubbing = thenReturn(value);
if (values == null) {
// For no good reason we're configuring null answer here
// This has been like that since forever, so let's keep it for compatibility (unless users complain)
return stubbing.thenReturn(null);
}
for (T v : values) {
stubbing = stubbing.thenReturn(v);
}
return stubbing;
}
// org.mockito.internal.stubbing.OngoingStubbingImpl
public OngoingStubbing<T> thenAnswer(Answer<?> answer) {
// 判断收否存在待打桩的调用
if(!invocationContainer.hasInvocationForPotentialStubbing()) {
throw incorrectUseOfApi();
}
// 往stubbed中添加打桩行为
invocationContainer.addAnswer(answer, strictness);
return new ConsecutiveStubbing<T>(invocationContainer);
}