diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index c4117c5d4d3..97f1d6145c9 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -16,6 +16,7 @@ by default, exactly in the following order: Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`after`" modes. +* `CommonCacheTestExecutionListener`: Clears application context cache if necessary. * `TransactionalTestExecutionListener`: Provides transactional test execution with default rollback semantics. * `SqlScriptsTestExecutionListener`: Runs SQL scripts configured by using the `@Sql` diff --git a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java new file mode 100644 index 00000000000..c61cadb68c1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2024 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.test.context.support; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.test.context.TestContext; + +/** + * {@code TestExecutionListener} which makes sure that caches are cleared once + * they are no longer required. Clears the resource cache of the + * {@link ApplicationContext} as it is only required during the beans + * initialization phase. Runs after {@link DirtiesContextTestExecutionListener} + * as dirtying the context will remove it from the cache and make this + * unnecessary. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class CommonCacheTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Returns {@code 3005}. + */ + @Override + public final int getOrder() { + return 3005; + } + + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + if (testContext.hasApplicationContext()) { + ApplicationContext applicationContext = testContext.getApplicationContext(); + if (applicationContext instanceof AbstractApplicationContext ctx) { + ctx.clearResourceCaches(); + } + } + } + +} diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index 570054a05e8..9bfc8ed723b 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -6,6 +6,7 @@ org.springframework.test.context.TestExecutionListener = \ org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\ org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener,\ org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\ + org.springframework.test.context.support.CommonCacheTestExecutionListener,\ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextTestExecutionListener,\ org.springframework.test.context.transaction.TransactionalTestExecutionListener,\ diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 728c6c9db66..eba96c3c2c0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -32,6 +32,7 @@ import org.springframework.test.context.event.ApplicationEventsTestExecutionList import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.CommonCacheTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; @@ -72,6 +73,7 @@ class TestExecutionListenersTests { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -94,6 +96,7 @@ class TestExecutionListenersTests { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -115,6 +118,7 @@ class TestExecutionListenersTests { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -138,6 +142,7 @@ class TestExecutionListenersTests { BarTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java new file mode 100644 index 00000000000..66912a53fd7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2024 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.test.context.cache; + +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.test.context.cache.SpringExtensionCommonCacheTests.TestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that verify that common caches are cleared at the end of a test + * class. Regular callback cannot be used to validate this as they run + * before the listener, so we need two test classes that are ordered to + * validate the result. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig(classes = TestConfiguration.class) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +class SpringExtensionCommonCacheTests { + + @Autowired + AbstractApplicationContext applicationContext; + + @Nested + @Order(1) + class FirstTests { + + @Test + void lazyInitBeans() { + applicationContext.getBean(String.class); + assertThat(applicationContext.getResourceCache(MetadataReader.class)).isNotEmpty(); + } + + } + + @Nested + @Order(2) + class SecondTests { + + @Test + void validateCommonCacheIsCleared() { + assertThat(applicationContext.getResourceCache(MetadataReader.class)).isEmpty(); + } + + } + + + @Configuration + static class TestConfiguration { + + @Bean + @Lazy + String dummyBean(ResourceLoader resourceLoader) { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(true); + scanner.setResourceLoader(resourceLoader); + scanner.findCandidateComponents(TestConfiguration.class.getPackageName()); + return "Dummy"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java new file mode 100644 index 00000000000..1209df579c5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2024 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.test.context.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.test.context.TestContext; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link CommonCacheTestExecutionListener}. + * + * @author Stephane Nicoll + */ +class CommonCacheTestExecutionListenerTests { + + private final CommonCacheTestExecutionListener listener = new CommonCacheTestExecutionListener(); + + @Test + void afterTestClassWhenContextIsAvailable() throws Exception { + AbstractApplicationContext applicationContext = mock(AbstractApplicationContext.class); + TestContext testContext = mock(TestContext.class); + given(testContext.hasApplicationContext()).willReturn(true); + given(testContext.getApplicationContext()).willReturn(applicationContext); + listener.afterTestClass(testContext); + verify(applicationContext).clearResourceCaches(); + } + + @Test + void afterTestClassCWhenContextIsNotAvailable() throws Exception { + TestContext testContext = mock(TestContext.class); + given(testContext.hasApplicationContext()).willReturn(false); + listener.afterTestClass(testContext); + verify(testContext).hasApplicationContext(); + verifyNoMoreInteractions(testContext); + } + +}