diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index 22731ae3e74..c87a9e6a711 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -26,6 +26,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; @@ -433,6 +434,8 @@ public class MockitoPostProcessor implements InstantiationAwareBeanPostProcessor private static final String BEAN_NAME = SpyPostProcessor.class.getName(); + private final Map earlySpyReferences = new ConcurrentHashMap<>(16); + private final MockitoPostProcessor mockitoPostProcessor; SpyPostProcessor(MockitoPostProcessor mockitoPostProcessor) { @@ -446,6 +449,10 @@ public class MockitoPostProcessor implements InstantiationAwareBeanPostProcessor @Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { + if (bean instanceof FactoryBean) { + return bean; + } + this.earlySpyReferences.put(getCacheKey(bean, beanName), bean); return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName); } @@ -454,7 +461,14 @@ public class MockitoPostProcessor implements InstantiationAwareBeanPostProcessor if (bean instanceof FactoryBean) { return bean; } - return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName); + if (this.earlySpyReferences.remove(getCacheKey(bean, beanName)) != bean) { + return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName); + } + return bean; + } + + private String getCacheKey(Object bean, String beanName) { + return StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName(); } static void register(BeanDefinitionRegistry registry) { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java new file mode 100644 index 00000000000..f751e19e3f7 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2022 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 + * + * https://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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.SpyBeanOnTestFieldForExistingCircularBeansConfig; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.mockito.BDDMockito.then; + +/** + * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing + * beans with circular dependencies. + * + * @author Andy Wilkinson + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = SpyBeanOnTestFieldForExistingCircularBeansConfig.class) +class SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests { + + @SpyBean + private One one; + + @Autowired + private Two two; + + @Test + void beanWithCircularDependenciesCanBeSpied() { + this.two.callOne(); + then(this.one).should().someMethod(); + } + + @Import({ One.class, Two.class }) + static class SpyBeanOnTestFieldForExistingCircularBeansConfig { + + } + + static class One { + + @Autowired + @SuppressWarnings("unused") + private Two two; + + void someMethod() { + + } + + } + + static class Two { + + @Autowired + private One one; + + void callOne() { + this.one.someMethod(); + } + + } + +}