Skip to content

Commit

Permalink
Allow @MockBean/@SpyBean on Spring AOP proxies
Browse files Browse the repository at this point in the history
Update Mockito support so that AOP Proxies automatically get additional
`Advice` that allows them to work with Mockito. Prior to this commit a
call to `verify` would fail because exiting AOP advice would confuse
Mockito and an `UnfinishedVerificationException` would be thrown.

The `MockitoAopProxyTargetInterceptor` works by detecting calls to a
mock that have been proceeded by `verify()` and bypassing AOP to
directly call the mock.

The order that `@SpyBean` creation occurs has also been updated to
ensure that that the spy is created before AOP advice is applied.
Without this, the creation of a spy would fail because Mockito copies
'state' to the newly created spied instance. Unfortunately, in the case
of AOP proxies, 'state' includes cglib interceptor fields. This means
that Mockito's own interceptors are clobbered by Spring's AOP
interceptors.

Fixes gh-5837
  • Loading branch information
philwebb committed May 12, 2016
1 parent 500edea commit cdfbf28
Show file tree
Hide file tree
Showing 16 changed files with 683 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ abstract class Definition {

private final MockReset reset;

Definition(String name, MockReset reset) {
private final boolean proxyTargetAware;

Definition(String name, MockReset reset, boolean proxyTargetAware) {
this.name = name;
this.reset = (reset != null ? reset : MockReset.AFTER);
this.proxyTargetAware = proxyTargetAware;
}

/**
Expand All @@ -53,11 +56,21 @@ public MockReset getReset() {
return this.reset;
}

/**
* Return if AOP advised beans should be proxy target aware.
* @return if proxy target aware
*/
public boolean isProxyTargetAware() {
return this.proxyTargetAware;
}

@Override
public int hashCode() {
int result = 1;
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
result = MULTIPLIER * result
+ ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
return result;
}

Expand All @@ -73,6 +86,8 @@ public boolean equals(Object obj) {
boolean result = true;
result &= ObjectUtils.nullSafeEquals(this.name, other.name);
result &= ObjectUtils.nullSafeEquals(this.reset, other.reset);
result &= ObjectUtils.nullSafeEquals(this.proxyTargetAware,
other.proxyTargetAware);
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement eleme
for (Class<?> classToMock : classesToMock) {
MockDefinition definition = new MockDefinition(annotation.name(), classToMock,
annotation.extraInterfaces(), annotation.answer(),
annotation.serializable(), annotation.reset());
annotation.serializable(), annotation.reset(),
annotation.proxyTargetAware());
addDefinition(element, definition, "mock");
}
}
Expand All @@ -107,7 +108,7 @@ private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element
}
for (Class<?> classToSpy : classesToSpy) {
SpyDefinition definition = new SpyDefinition(annotation.name(), classToSpy,
annotation.reset());
annotation.reset(), annotation.proxyTargetAware());
addDefinition(element, definition, "spy");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.MockSettings;
import org.mockito.Mockito;

import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
Expand Down Expand Up @@ -139,4 +140,15 @@
*/
MockReset reset() default MockReset.AFTER;

/**
* Indicates that Mockito methods such as {@link Mockito#verify(Object) verify(mock)}
* should use the {@code target} of AOP advised beans, rather than the proxy itself.
* If set to {@code false} you may need to use the result of
* {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object)
* AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods.
* @return {@code true} if the target of AOP advised beans is used or {@code false} if
* the proxy is used directly
*/
boolean proxyTargetAware() default true;

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ class MockDefinition extends Definition {
private final boolean serializable;

MockDefinition(Class<?> classToMock) {
this(null, classToMock, null, null, false, null);
this(null, classToMock, null, null, false, null, true);
}

MockDefinition(String name, Class<?> classToMock, Class<?>[] extraInterfaces,
Answers answer, boolean serializable, MockReset reset) {
super(name, reset);
Answers answer, boolean serializable, MockReset reset,
boolean proxyTargetAware) {
super(name, reset, proxyTargetAware);
Assert.notNull(classToMock, "ClassToMock must not be null");
this.classToMock = classToMock;
this.extraInterfaces = asClassSet(extraInterfaces);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* the {@code ApplicationContext} using the static methods.
*
* @author Phillip Webb
* @since 1.4.0
* @see ResetMocksTestExecutionListener
*/
public enum MockReset {
Expand All @@ -55,26 +56,26 @@ public enum MockReset {
private static final MockUtil util = new MockUtil();

/**
* Create {@link MockSettings settings} to be used with mocks where reset should
* occur before each test method runs.
* Create {@link MockSettings settings} to be used with mocks where reset should occur
* before each test method runs.
* @return mock settings
*/
public static MockSettings before() {
return withSettings(BEFORE);
}

/**
* Create {@link MockSettings settings} to be used with mocks where reset should
* occur after each test method runs.
* Create {@link MockSettings settings} to be used with mocks where reset should occur
* after each test method runs.
* @return mock settings
*/
public static MockSettings after() {
return withSettings(AFTER);
}

/**
* Create {@link MockSettings settings} to be used with mocks where a specific
* reset should occur.
* Create {@link MockSettings settings} to be used with mocks where a specific reset
* should occur.
* @param reset the reset type
* @return mock settings
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.test.mock.mockito;

import java.lang.reflect.Field;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.Interceptor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.mockito.internal.InternalMockHandler;
import org.mockito.internal.progress.MockingProgress;
import org.mockito.internal.stubbing.InvocationContainer;
import org.mockito.internal.util.MockUtil;
import org.mockito.internal.verification.MockAwareVerificationMode;
import org.mockito.verification.VerificationMode;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.AopTestUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
* AOP {@link Interceptor} that attempts to make AOP proxy beans work with Mockito. Works
* by bypassing AOP advice when a method is invoked via
* {@code Mockito#verify(Object) verify(mock)}.
*
* @author Phillip Webb
*/
class MockitoAopProxyTargetInterceptor implements MethodInterceptor {

private final Object source;

private final Object target;

private final Verification verification;

MockitoAopProxyTargetInterceptor(Object source, Object target) throws Exception {
this.source = source;
this.target = target;
this.verification = new Verification(target);
}

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (this.verification.isVerifying()) {
this.verification.replaceVerifyMock(this.source, this.target);
return AopUtils.invokeJoinpointUsingReflection(this.target,
invocation.getMethod(), invocation.getArguments());
}
return invocation.proceed();
}

@Autowired
public static void applyTo(Object source) {
Assert.state(AopUtils.isAopProxy(source), "Source must be an AOP proxy");
try {
Advised advised = (Advised) source;
for (Advisor advisor : advised.getAdvisors()) {
if (advisor instanceof MockitoAopProxyTargetInterceptor) {
return;
}
}
Object target = AopTestUtils.getUltimateTargetObject(source);
Advice advice = new MockitoAopProxyTargetInterceptor(source, target);
advised.addAdvice(0, advice);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to apply Mockito AOP support", ex);
}
}

private static class Verification {

private final MockingProgress progress;

Verification(Object target) {
MockUtil mockUtil = new MockUtil();
InternalMockHandler<?> handler = mockUtil.getMockHandler(target);
InvocationContainer container = handler.getInvocationContainer();
Field field = ReflectionUtils.findField(container.getClass(),
"mockingProgress");
ReflectionUtils.makeAccessible(field);
this.progress = (MockingProgress) ReflectionUtils.getField(field, container);
}

public synchronized boolean isVerifying() {
VerificationMode mode = this.progress.pullVerificationMode();
if (mode != null) {
this.progress.verificationStarted(mode);
return true;
}
return false;
}

public synchronized void replaceVerifyMock(Object source, Object target) {
VerificationMode mode = this.progress.pullVerificationMode();
if (mode != null) {
if (mode instanceof MockAwareVerificationMode) {
MockAwareVerificationMode mockAwareMode = (MockAwareVerificationMode) mode;
if (mockAwareMode.getMock() == source) {
mode = new MockAwareVerificationMode(target, mockAwareMode);
}
}
this.progress.verificationStarted(mode);
}
}

}

}
Loading

0 comments on commit cdfbf28

Please sign in to comment.