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
This commit is contained in:
Andy Wilkinson 2020-07-27 12:52:42 +01:00
parent 16eaae0b2f
commit 41954533b2
2 changed files with 136 additions and 1 deletions

View File

@ -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;
}

View File

@ -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<MergedContextConfiguration, ApplicationContext> contexts = (Map<MergedContextConfiguration, ApplicationContext>) 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 {
}
}