From 41954533b28769ae82664d80e4f58b3a210dcb64 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Jul 2020 12:52:42 +0100 Subject: [PATCH] Fix Mock|SpyBean context caching The fix for gh-20916 updated DefinitionsParser so that the ResolvableType for each MockBean or SpyBean field included the implementation class from which the field was found. Where the field was declared with a variable generic signature that was made constant by its implementation class, this allowed the correct concrete type to be determined. It also had the unintended side-effect of preventing two test classes with identical `@MockBean` and `@SpyBean` configuration from sharing a context as the resolvable types for their mock and spy bean fields would now be different. This commit updates DefinitionsParser to only include the implementation class in the ResolvableType if the field's generic type is variable. For cases where it is not variable, this restores the behaviour prior to the fix for gh-20916. Fixes gh-22566 --- .../test/mock/mockito/DefinitionsParser.java | 9 +- .../mockito/MockBeanContextCachingTests.java | 128 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java index 656aa46d01..a9622acb6a 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java @@ -18,6 +18,7 @@ package org.springframework.boot.test.mock.mockito; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -114,7 +115,13 @@ class DefinitionsParser { types.add(ResolvableType.forClass(clazz)); } if (types.isEmpty() && element instanceof Field) { - types.add(ResolvableType.forField((Field) element, source)); + Field field = (Field) element; + if (field.getGenericType() instanceof TypeVariable) { + types.add(ResolvableType.forField(field, source)); + } + else { + types.add(ResolvableType.forField(field)); + } } return types; } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java new file mode 100644 index 0000000000..5d84902dda --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2020 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 java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate; +import org.springframework.test.context.cache.DefaultContextCache; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for application context caching when using {@link MockBean @MockBean}. + * + * @author Andy Wilkinson + */ +class MockBeanContextCachingTests { + + private final DefaultContextCache contextCache = new DefaultContextCache(); + + private final DefaultCacheAwareContextLoaderDelegate delegate = new DefaultCacheAwareContextLoaderDelegate( + this.contextCache); + + @AfterEach + @SuppressWarnings("unchecked") + void clearCache() { + Map contexts = (Map) ReflectionTestUtils + .getField(this.contextCache, "contextMap"); + for (ApplicationContext context : contexts.values()) { + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + } + this.contextCache.clear(); + } + + @Test + void whenThereIsANormalBeanAndAMockBeanThenTwoContextsAreCreated() { + bootstrapContext(TestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + bootstrapContext(MockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(2); + } + + @Test + void whenThereIsTheSameMockedBeanInEachTestClassThenOneContextIsCreated() { + bootstrapContext(MockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + bootstrapContext(AnotherMockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + } + + @SuppressWarnings("rawtypes") + private void bootstrapContext(Class testClass) { + SpringBootTestContextBootstrapper bootstrapper = new SpringBootTestContextBootstrapper(); + BootstrapContext bootstrapContext = mock(BootstrapContext.class); + given((Class) bootstrapContext.getTestClass()).willReturn(testClass); + bootstrapper.setBootstrapContext(bootstrapContext); + given(bootstrapContext.getCacheAwareContextLoaderDelegate()).willReturn(this.delegate); + TestContext testContext = bootstrapper.buildTestContext(); + testContext.getApplicationContext(); + } + + @SpringBootTest(classes = TestConfiguration.class) + static class TestClass { + + } + + @SpringBootTest(classes = TestConfiguration.class) + static class MockedBeanTestClass { + + @MockBean + private TestBean testBean; + + } + + @SpringBootTest(classes = TestConfiguration.class) + static class AnotherMockedBeanTestClass { + + @MockBean + private TestBean testBean; + + } + + @Configuration + static class TestConfiguration { + + @Bean + TestBean testBean() { + return new TestBean(); + } + + } + + static class TestBean { + + } + +}