Skip to content

Commit cdfbf28

Browse files
committed
Allow @MockBean/@SpyBean on Spring AOP proxies
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
1 parent 500edea commit cdfbf28

16 files changed

+683
-61
lines changed

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/Definition.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ abstract class Definition {
3232

3333
private final MockReset reset;
3434

35-
Definition(String name, MockReset reset) {
35+
private final boolean proxyTargetAware;
36+
37+
Definition(String name, MockReset reset, boolean proxyTargetAware) {
3638
this.name = name;
3739
this.reset = (reset != null ? reset : MockReset.AFTER);
40+
this.proxyTargetAware = proxyTargetAware;
3841
}
3942

4043
/**
@@ -53,11 +56,21 @@ public MockReset getReset() {
5356
return this.reset;
5457
}
5558

59+
/**
60+
* Return if AOP advised beans should be proxy target aware.
61+
* @return if proxy target aware
62+
*/
63+
public boolean isProxyTargetAware() {
64+
return this.proxyTargetAware;
65+
}
66+
5667
@Override
5768
public int hashCode() {
5869
int result = 1;
5970
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
6071
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
72+
result = MULTIPLIER * result
73+
+ ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
6174
return result;
6275
}
6376

@@ -73,6 +86,8 @@ public boolean equals(Object obj) {
7386
boolean result = true;
7487
result &= ObjectUtils.nullSafeEquals(this.name, other.name);
7588
result &= ObjectUtils.nullSafeEquals(this.reset, other.reset);
89+
result &= ObjectUtils.nullSafeEquals(this.proxyTargetAware,
90+
other.proxyTargetAware);
7691
return result;
7792
}
7893

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement eleme
9292
for (Class<?> classToMock : classesToMock) {
9393
MockDefinition definition = new MockDefinition(annotation.name(), classToMock,
9494
annotation.extraInterfaces(), annotation.answer(),
95-
annotation.serializable(), annotation.reset());
95+
annotation.serializable(), annotation.reset(),
96+
annotation.proxyTargetAware());
9697
addDefinition(element, definition, "mock");
9798
}
9899
}
@@ -107,7 +108,7 @@ private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element
107108
}
108109
for (Class<?> classToSpy : classesToSpy) {
109110
SpyDefinition definition = new SpyDefinition(annotation.name(), classToSpy,
110-
annotation.reset());
111+
annotation.reset(), annotation.proxyTargetAware());
111112
addDefinition(element, definition, "spy");
112113
}
113114
}

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockBean.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.runner.RunWith;
2727
import org.mockito.Answers;
2828
import org.mockito.MockSettings;
29+
import org.mockito.Mockito;
2930

3031
import org.springframework.context.ApplicationContext;
3132
import org.springframework.core.annotation.AliasFor;
@@ -139,4 +140,15 @@
139140
*/
140141
MockReset reset() default MockReset.AFTER;
141142

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

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockDefinition.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ class MockDefinition extends Definition {
4848
private final boolean serializable;
4949

5050
MockDefinition(Class<?> classToMock) {
51-
this(null, classToMock, null, null, false, null);
51+
this(null, classToMock, null, null, false, null, true);
5252
}
5353

5454
MockDefinition(String name, Class<?> classToMock, Class<?>[] extraInterfaces,
55-
Answers answer, boolean serializable, MockReset reset) {
56-
super(name, reset);
55+
Answers answer, boolean serializable, MockReset reset,
56+
boolean proxyTargetAware) {
57+
super(name, reset, proxyTargetAware);
5758
Assert.notNull(classToMock, "ClassToMock must not be null");
5859
this.classToMock = classToMock;
5960
this.extraInterfaces = asClassSet(extraInterfaces);

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockReset.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* the {@code ApplicationContext} using the static methods.
3434
*
3535
* @author Phillip Webb
36+
* @since 1.4.0
3637
* @see ResetMocksTestExecutionListener
3738
*/
3839
public enum MockReset {
@@ -55,26 +56,26 @@ public enum MockReset {
5556
private static final MockUtil util = new MockUtil();
5657

5758
/**
58-
* Create {@link MockSettings settings} to be used with mocks where reset should
59-
* occur before each test method runs.
59+
* Create {@link MockSettings settings} to be used with mocks where reset should occur
60+
* before each test method runs.
6061
* @return mock settings
6162
*/
6263
public static MockSettings before() {
6364
return withSettings(BEFORE);
6465
}
6566

6667
/**
67-
* Create {@link MockSettings settings} to be used with mocks where reset should
68-
* occur after each test method runs.
68+
* Create {@link MockSettings settings} to be used with mocks where reset should occur
69+
* after each test method runs.
6970
* @return mock settings
7071
*/
7172
public static MockSettings after() {
7273
return withSettings(AFTER);
7374
}
7475

7576
/**
76-
* Create {@link MockSettings settings} to be used with mocks where a specific
77-
* reset should occur.
77+
* Create {@link MockSettings settings} to be used with mocks where a specific reset
78+
* should occur.
7879
* @param reset the reset type
7980
* @return mock settings
8081
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.mock.mockito;
18+
19+
import java.lang.reflect.Field;
20+
21+
import org.aopalliance.aop.Advice;
22+
import org.aopalliance.intercept.Interceptor;
23+
import org.aopalliance.intercept.MethodInterceptor;
24+
import org.aopalliance.intercept.MethodInvocation;
25+
import org.mockito.internal.InternalMockHandler;
26+
import org.mockito.internal.progress.MockingProgress;
27+
import org.mockito.internal.stubbing.InvocationContainer;
28+
import org.mockito.internal.util.MockUtil;
29+
import org.mockito.internal.verification.MockAwareVerificationMode;
30+
import org.mockito.verification.VerificationMode;
31+
32+
import org.springframework.aop.Advisor;
33+
import org.springframework.aop.framework.Advised;
34+
import org.springframework.aop.support.AopUtils;
35+
import org.springframework.beans.factory.annotation.Autowired;
36+
import org.springframework.test.util.AopTestUtils;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.ReflectionUtils;
39+
40+
/**
41+
* AOP {@link Interceptor} that attempts to make AOP proxy beans work with Mockito. Works
42+
* by bypassing AOP advice when a method is invoked via
43+
* {@code Mockito#verify(Object) verify(mock)}.
44+
*
45+
* @author Phillip Webb
46+
*/
47+
class MockitoAopProxyTargetInterceptor implements MethodInterceptor {
48+
49+
private final Object source;
50+
51+
private final Object target;
52+
53+
private final Verification verification;
54+
55+
MockitoAopProxyTargetInterceptor(Object source, Object target) throws Exception {
56+
this.source = source;
57+
this.target = target;
58+
this.verification = new Verification(target);
59+
}
60+
61+
@Override
62+
public Object invoke(MethodInvocation invocation) throws Throwable {
63+
if (this.verification.isVerifying()) {
64+
this.verification.replaceVerifyMock(this.source, this.target);
65+
return AopUtils.invokeJoinpointUsingReflection(this.target,
66+
invocation.getMethod(), invocation.getArguments());
67+
}
68+
return invocation.proceed();
69+
}
70+
71+
@Autowired
72+
public static void applyTo(Object source) {
73+
Assert.state(AopUtils.isAopProxy(source), "Source must be an AOP proxy");
74+
try {
75+
Advised advised = (Advised) source;
76+
for (Advisor advisor : advised.getAdvisors()) {
77+
if (advisor instanceof MockitoAopProxyTargetInterceptor) {
78+
return;
79+
}
80+
}
81+
Object target = AopTestUtils.getUltimateTargetObject(source);
82+
Advice advice = new MockitoAopProxyTargetInterceptor(source, target);
83+
advised.addAdvice(0, advice);
84+
}
85+
catch (Exception ex) {
86+
throw new IllegalStateException("Unable to apply Mockito AOP support", ex);
87+
}
88+
}
89+
90+
private static class Verification {
91+
92+
private final MockingProgress progress;
93+
94+
Verification(Object target) {
95+
MockUtil mockUtil = new MockUtil();
96+
InternalMockHandler<?> handler = mockUtil.getMockHandler(target);
97+
InvocationContainer container = handler.getInvocationContainer();
98+
Field field = ReflectionUtils.findField(container.getClass(),
99+
"mockingProgress");
100+
ReflectionUtils.makeAccessible(field);
101+
this.progress = (MockingProgress) ReflectionUtils.getField(field, container);
102+
}
103+
104+
public synchronized boolean isVerifying() {
105+
VerificationMode mode = this.progress.pullVerificationMode();
106+
if (mode != null) {
107+
this.progress.verificationStarted(mode);
108+
return true;
109+
}
110+
return false;
111+
}
112+
113+
public synchronized void replaceVerifyMock(Object source, Object target) {
114+
VerificationMode mode = this.progress.pullVerificationMode();
115+
if (mode != null) {
116+
if (mode instanceof MockAwareVerificationMode) {
117+
MockAwareVerificationMode mockAwareMode = (MockAwareVerificationMode) mode;
118+
if (mockAwareMode.getMock() == source) {
119+
mode = new MockAwareVerificationMode(target, mockAwareMode);
120+
}
121+
}
122+
this.progress.verificationStarted(mode);
123+
}
124+
}
125+
126+
}
127+
128+
}

0 commit comments

Comments
 (0)