-
Notifications
You must be signed in to change notification settings - Fork 40.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
16 changed files
with
683 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
...ain/java/org/springframework/boot/test/mock/mockito/MockitoAopProxyTargetInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
} | ||
|
||
} |
Oops, something went wrong.