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
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.exceptions.misusing.UnfinishedVerificationException;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.interceptor.CacheResolver;
|
||||
import org.springframework.cache.interceptor.SimpleCacheResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test {@link MockBean} when mixed with Spring AOP.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see <a href="https://github.com/spring-projects/spring-boot/issues/5837">5837</a>
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
public class MockBeanWithAopProxyAndNotProxyTargetAwareTests {
|
||||
|
||||
@MockBean(proxyTargetAware = false)
|
||||
private DateService dateService;
|
||||
|
||||
@Test(expected = UnfinishedVerificationException.class)
|
||||
public void verifyShouldUseProxyTarget() throws Exception {
|
||||
this.dateService.getDate();
|
||||
verify(this.dateService, times(1)).getDate();
|
||||
reset(this.dateService);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCaching(proxyTargetClass = true)
|
||||
@Import(DateService.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public CacheResolver cacheResolver(CacheManager cacheManager) {
|
||||
SimpleCacheResolver resolver = new SimpleCacheResolver();
|
||||
resolver.setCacheManager(cacheManager);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMapCacheManager cacheManager() {
|
||||
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
|
||||
cacheManager.setCacheNames(Arrays.asList("test"));
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service
|
||||
static class DateService {
|
||||
|
||||
@Cacheable(cacheNames = "test")
|
||||
public Long getDate() {
|
||||
return System.nanoTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.interceptor.CacheResolver;
|
||||
import org.springframework.cache.interceptor.SimpleCacheResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test {@link MockBean} when mixed with Spring AOP.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see <a href="https://github.com/spring-projects/spring-boot/issues/5837">5837</a>
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
public class MockBeanWithAopProxyTests {
|
||||
|
||||
@MockBean
|
||||
private DateService dateService;
|
||||
|
||||
@Test
|
||||
public void verifyShouldUseProxyTarget() throws Exception {
|
||||
Long d1 = this.dateService.getDate();
|
||||
Thread.sleep(200);
|
||||
Long d2 = this.dateService.getDate();
|
||||
assertThat(d1).isEqualTo(d2);
|
||||
verify(this.dateService, times(1)).getDate();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCaching(proxyTargetClass = true)
|
||||
@Import(DateService.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public CacheResolver cacheResolver(CacheManager cacheManager) {
|
||||
SimpleCacheResolver resolver = new SimpleCacheResolver();
|
||||
resolver.setCacheManager(cacheManager);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMapCacheManager cacheManager() {
|
||||
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
|
||||
cacheManager.setCacheNames(Arrays.asList("test"));
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service
|
||||
static class DateService {
|
||||
|
||||
@Cacheable(cacheNames = "test")
|
||||
public Long getDate() {
|
||||
return System.nanoTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,13 +42,13 @@ public class MockDefinitionTests {
|
||||
public void ClassToMockMustNotBeNull() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("ClassToMock must not be null");
|
||||
new MockDefinition(null, null, null, null, false, null);
|
||||
new MockDefinition(null, null, null, null, false, null, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWithDefaults() throws Exception {
|
||||
MockDefinition definition = new MockDefinition(null, ExampleService.class, null,
|
||||
null, false, null);
|
||||
null, false, null, true);
|
||||
assertThat(definition.getName()).isNull();
|
||||
assertThat(definition.getClassToMock()).isEqualTo(ExampleService.class);
|
||||
assertThat(definition.getExtraInterfaces()).isEmpty();
|
||||
@@ -61,7 +61,7 @@ public class MockDefinitionTests {
|
||||
public void createExplicit() throws Exception {
|
||||
MockDefinition definition = new MockDefinition("name", ExampleService.class,
|
||||
new Class<?>[] { ExampleExtraInterface.class },
|
||||
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE);
|
||||
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE, false);
|
||||
assertThat(definition.getName()).isEqualTo("name");
|
||||
assertThat(definition.getClassToMock()).isEqualTo(ExampleService.class);
|
||||
assertThat(definition.getExtraInterfaces())
|
||||
@@ -69,13 +69,14 @@ public class MockDefinitionTests {
|
||||
assertThat(definition.getAnswer()).isEqualTo(Answers.RETURNS_SMART_NULLS);
|
||||
assertThat(definition.isSerializable()).isTrue();
|
||||
assertThat(definition.getReset()).isEqualTo(MockReset.BEFORE);
|
||||
assertThat(definition.isProxyTargetAware()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMock() throws Exception {
|
||||
MockDefinition definition = new MockDefinition("name", ExampleService.class,
|
||||
new Class<?>[] { ExampleExtraInterface.class },
|
||||
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE);
|
||||
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE, true);
|
||||
ExampleService mock = definition.createMock();
|
||||
MockCreationSettings<?> settings = new MockUtil().getMockSettings(mock);
|
||||
assertThat(mock).isInstanceOf(ExampleService.class);
|
||||
@@ -85,7 +86,6 @@ public class MockDefinitionTests {
|
||||
.isEqualTo(Answers.RETURNS_SMART_NULLS.get());
|
||||
assertThat(settings.isSerializable()).isTrue();
|
||||
assertThat(MockReset.get(mock)).isEqualTo(MockReset.BEFORE);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.exceptions.misusing.UnfinishedVerificationException;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.interceptor.CacheResolver;
|
||||
import org.springframework.cache.interceptor.SimpleCacheResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test {@link SpyBean} when mixed with Spring AOP.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see <a href="https://github.com/spring-projects/spring-boot/issues/5837">5837</a>
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
public class SpyBeanWithAopProxyAndNotProxyTargetAwareTests {
|
||||
|
||||
@SpyBean(proxyTargetAware = false)
|
||||
private DateService dateService;
|
||||
|
||||
@Test(expected = UnfinishedVerificationException.class)
|
||||
public void verifyShouldUseProxyTarget() throws Exception {
|
||||
this.dateService.getDate();
|
||||
verify(this.dateService, times(1)).getDate();
|
||||
reset(this.dateService);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCaching(proxyTargetClass = true)
|
||||
@Import(DateService.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public CacheResolver cacheResolver(CacheManager cacheManager) {
|
||||
SimpleCacheResolver resolver = new SimpleCacheResolver();
|
||||
resolver.setCacheManager(cacheManager);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMapCacheManager cacheManager() {
|
||||
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
|
||||
cacheManager.setCacheNames(Arrays.asList("test"));
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service
|
||||
static class DateService {
|
||||
|
||||
@Cacheable(cacheNames = "test")
|
||||
public Long getDate() {
|
||||
return System.nanoTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.interceptor.CacheResolver;
|
||||
import org.springframework.cache.interceptor.SimpleCacheResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test {@link SpyBean} when mixed with Spring AOP.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see <a href="https://github.com/spring-projects/spring-boot/issues/5837">5837</a>
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
public class SpyBeanWithAopProxyTests {
|
||||
|
||||
@SpyBean
|
||||
private DateService dateService;
|
||||
|
||||
@Test
|
||||
public void verifyShouldUseProxyTarget() throws Exception {
|
||||
Long d1 = this.dateService.getDate();
|
||||
Thread.sleep(200);
|
||||
Long d2 = this.dateService.getDate();
|
||||
assertThat(d1).isEqualTo(d2);
|
||||
verify(this.dateService, times(1)).getDate();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCaching(proxyTargetClass = true)
|
||||
@Import(DateService.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public CacheResolver cacheResolver(CacheManager cacheManager) {
|
||||
SimpleCacheResolver resolver = new SimpleCacheResolver();
|
||||
resolver.setCacheManager(cacheManager);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMapCacheManager cacheManager() {
|
||||
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
|
||||
cacheManager.setCacheNames(Arrays.asList("test"));
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service
|
||||
static class DateService {
|
||||
|
||||
@Cacheable(cacheNames = "test")
|
||||
public Long getDate() {
|
||||
return System.nanoTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,31 +43,33 @@ public class SpyDefinitionTests {
|
||||
public void classToSpyMustNotBeNull() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("ClassToSpy must not be null");
|
||||
new SpyDefinition(null, null, null);
|
||||
new SpyDefinition(null, null, null, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWithDefaults() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition(null, RealExampleService.class,
|
||||
null);
|
||||
SpyDefinition definition = new SpyDefinition(null, RealExampleService.class, null,
|
||||
true);
|
||||
assertThat(definition.getName()).isNull();
|
||||
assertThat(definition.getClassToSpy()).isEqualTo(RealExampleService.class);
|
||||
assertThat(definition.getReset()).isEqualTo(MockReset.AFTER);
|
||||
assertThat(definition.isProxyTargetAware()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExplicit() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition("name", RealExampleService.class,
|
||||
MockReset.BEFORE);
|
||||
MockReset.BEFORE, false);
|
||||
assertThat(definition.getName()).isEqualTo("name");
|
||||
assertThat(definition.getClassToSpy()).isEqualTo(RealExampleService.class);
|
||||
assertThat(definition.getReset()).isEqualTo(MockReset.BEFORE);
|
||||
assertThat(definition.isProxyTargetAware()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSpy() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition("name", RealExampleService.class,
|
||||
MockReset.BEFORE);
|
||||
MockReset.BEFORE, true);
|
||||
RealExampleService spy = definition.createSpy(new RealExampleService("hello"));
|
||||
MockCreationSettings<?> settings = new MockUtil().getMockSettings(spy);
|
||||
assertThat(spy).isInstanceOf(ExampleService.class);
|
||||
@@ -80,7 +82,7 @@ public class SpyDefinitionTests {
|
||||
@Test
|
||||
public void createSpyWhenNullInstanceShouldThrowException() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition("name", RealExampleService.class,
|
||||
MockReset.BEFORE);
|
||||
MockReset.BEFORE, true);
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Instance must not be null");
|
||||
definition.createSpy(null);
|
||||
@@ -89,7 +91,7 @@ public class SpyDefinitionTests {
|
||||
@Test
|
||||
public void createSpyWhenWrongInstanceShouldThrowException() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition("name", RealExampleService.class,
|
||||
MockReset.BEFORE);
|
||||
MockReset.BEFORE, true);
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("must be an instance of");
|
||||
definition.createSpy(new ExampleServiceCaller(null));
|
||||
@@ -98,7 +100,7 @@ public class SpyDefinitionTests {
|
||||
@Test
|
||||
public void createSpyTwice() throws Exception {
|
||||
SpyDefinition definition = new SpyDefinition("name", RealExampleService.class,
|
||||
MockReset.BEFORE);
|
||||
MockReset.BEFORE, true);
|
||||
Object instance = new RealExampleService("hello");
|
||||
instance = definition.createSpy(instance);
|
||||
instance = definition.createSpy(instance);
|
||||
|
||||
Reference in New Issue
Block a user