diff --git a/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java b/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java new file mode 100644 index 00000000000..a947cbbfddd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-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.test.context; + +import org.springframework.context.ApplicationContext; + +/** + * Strategy for components that process failures related to application contexts + * within the Spring TestContext Framework. + * + * @author Sam Brannen + * @since 6.0 + * @see ContextLoadException + */ +public interface ApplicationContextFailureProcessor { + + /** + * Invoked when a failure was encountered while attempting to load an + * {@link ApplicationContext}. + *

Implementations of this method must not throw any exceptions. Consequently, + * any exception thrown by an implementation of this method will be ignored. + * @param context the application context that did not load successfully + * @param exception the exception caught while loading the application context + */ + void processLoadFailure(ApplicationContext context, Throwable exception); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index efd6dca1780..d7086c3ae80 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -75,6 +75,7 @@ public interface CacheAwareContextLoaderDelegate { * the application context * @see #isContextLoaded * @see #closeContext + * @see #setContextFailureProcessor */ ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration); @@ -100,4 +101,18 @@ public interface CacheAwareContextLoaderDelegate { */ void closeContext(MergedContextConfiguration mergedContextConfiguration, @Nullable HierarchyMode hierarchyMode); + /** + * Set the {@link ApplicationContextFailureProcessor} to use. + *

The default implementation ignores the supplied processor. + *

Concrete implementations should override this method to store a reference + * to the supplied processor and use it to process {@link ContextLoadException + * ContextLoadExceptions} thrown from context loaders in + * {@link #loadContext(MergedContextConfiguration)}. + * @param contextFailureProcessor the context failure processor to use + * @since 6.0 + */ + default void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) { + // no-op + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoadException.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoadException.java new file mode 100644 index 00000000000..723d8ebb3c8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoadException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-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.test.context; + +import org.springframework.context.ApplicationContext; + +/** + * Exception thrown when an error occurs while a {@link SmartContextLoader} + * attempts to load an {@link ApplicationContext}. + * + *

This exception provides access to the {@linkplain #getApplicationContext() + * application context} that failed to load as well as the {@linkplain #getCause() + * exception} caught while attempting to load that context. + * + * @author Sam Brannen + * @since 6.0 + * @see SmartContextLoader#loadContext(MergedContextConfiguration) + */ +@SuppressWarnings("serial") +public class ContextLoadException extends Exception { + + private final ApplicationContext applicationContext; + + + /** + * Create a new {@code ContextLoadException} for the supplied + * {@link ApplicationContext} and {@link Exception}. + * @param applicationContext the application context that failed to load + * @param cause the exception caught while attempting to load that context + */ + public ContextLoadException(ApplicationContext applicationContext, Exception cause) { + super(cause); + this.applicationContext = applicationContext; + } + + + /** + * Get the {@code ApplicationContext} that failed to load. + *

Clients must not retain a long-lived reference to the context returned + * from this method. + */ + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java index 09091083cf8..3461a7b1355 100644 --- a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java @@ -126,10 +126,23 @@ public interface SmartContextLoader extends ContextLoader { * closed on JVM shutdown. This allows for freeing of external resources held * by beans within the context — for example, temporary files. * + *

As of Spring Framework 6.0, any exception thrown while attempting to + * load an {@code ApplicationContext} should be wrapped in a + * {@link ContextLoadException}. Concrete implementations should therefore + * contain a try-catch block similar to the following. + *

+	 * ApplicationContext context = // create context
+	 * try {
+	 *     // configure and refresh context
+	 * }
+	 * catch (Exception ex) {
+	 *     throw new ContextLoadException(context, ex);
+	 * }
+	 * 
* @param mergedConfig the merged context configuration to use to load the * application context * @return a new application context - * @throws Exception if context loading failed + * @throws ContextLoadException if context loading failed * @see #processContextConfiguration(ContextConfigurationAttributes) * @see #loadContextForAotProcessing(MergedContextConfiguration) * @see org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry) diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java index 6d9a53e1bb3..25d64980732 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java @@ -19,6 +19,7 @@ package org.springframework.test.context.aot; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; @@ -52,10 +53,22 @@ public interface AotContextLoader extends SmartContextLoader { * {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() * register a JVM shutdown hook} for it. Otherwise, this method should implement * behavior identical to {@code loadContext(MergedContextConfiguration)}. + *

Any exception thrown while attempting to load an {@code ApplicationContext} + * should be wrapped in a {@link ContextLoadException}. Concrete implementations + * should therefore contain a try-catch block similar to the following. + *

+	 * GenericApplicationContext context = // create context
+	 * try {
+	 *     // configure context
+	 * }
+	 * catch (Exception ex) {
+	 *     throw new ContextLoadException(context, ex);
+	 * }
+	 * 
* @param mergedConfig the merged context configuration to use to load the * application context * @return a new {@code GenericApplicationContext} - * @throws Exception if context loading failed + * @throws ContextLoadException if context loading failed * @see #loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer) */ ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception; @@ -67,12 +80,24 @@ public interface AotContextLoader extends SmartContextLoader { *

This method must instantiate, initialize, and * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() * refresh} the {@code ApplicationContext}. + *

Any exception thrown while attempting to load an {@code ApplicationContext} + * should be wrapped in a {@link ContextLoadException}. Concrete implementations + * should therefore contain a try-catch block similar to the following. + *

+	 * GenericApplicationContext context = // create context
+	 * try {
+	 *     // configure and refresh context
+	 * }
+	 * catch (Exception ex) {
+	 *     throw new ContextLoadException(context, ex);
+	 * }
+	 * 
* @param mergedConfig the merged context configuration to use to load the * application context * @param initializer the {@code ApplicationContextInitializer} that should * be applied to the context in order to recreate bean definitions * @return a new {@code GenericApplicationContext} - * @throws Exception if context loading failed + * @throws ContextLoadException if context loading failed * @see #loadContextForAotProcessing(MergedContextConfiguration) */ ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index 8b81e544d3d..fac0cfc6df2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -39,6 +39,7 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.javapoet.ClassName; import org.springframework.test.context.BootstrapUtils; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; @@ -217,9 +218,10 @@ public class TestContextAotGenerator { } } catch (Exception ex) { + Throwable cause = (ex instanceof ContextLoadException cle ? cle.getCause() : ex); throw new TestContextAotException( "Failed to load ApplicationContext for AOT processing for test class [%s]" - .formatted(testClass.getName()), ex); + .formatted(testClass.getName()), cause); } } throw new TestContextAotException(""" diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java index 6566ed4dfda..3a357dc62cb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java @@ -26,7 +26,9 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.ApplicationContextFailureProcessor; import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; @@ -59,6 +61,9 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext private final ContextCache contextCache; + @Nullable + private ApplicationContextFailureProcessor contextFailureProcessor; + /** * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using @@ -110,8 +115,23 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext this.contextCache.put(mergedContextConfiguration, context); } catch (Exception ex) { + Throwable cause = ex; + if (ex instanceof ContextLoadException cle) { + cause = cle.getCause(); + if (this.contextFailureProcessor != null) { + try { + this.contextFailureProcessor.processLoadFailure(cle.getApplicationContext(), cause); + } + catch (Throwable throwable) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring exception thrown from ApplicationContextFailureProcessor [%s]: %s" + .formatted(this.contextFailureProcessor, throwable)); + } + } + } + } throw new IllegalStateException( - "Failed to load ApplicationContext for " + mergedContextConfiguration, ex); + "Failed to load ApplicationContext for " + mergedContextConfiguration, cause); } } else { @@ -134,6 +154,12 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext } } + @Override + public void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) { + this.contextFailureProcessor = contextFailureProcessor; + } + + /** * Get the {@link ContextCache} used by this context loader delegate. */ diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index 48c68ae013c..950d7a6c0fd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.aot.AotContextLoader; import org.springframework.util.Assert; @@ -159,13 +160,18 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader validateMergedContextConfiguration(mergedConfig); GenericApplicationContext context = createContext(); - prepareContext(context); - prepareContext(context, mergedConfig); - initializer.initialize(context); - customizeContext(context); - customizeContext(context, mergedConfig); - context.refresh(); - return context; + try { + prepareContext(context); + prepareContext(context, mergedConfig); + initializer.initialize(context); + customizeContext(context); + customizeContext(context, mergedConfig); + context.refresh(); + return context; + } + catch (Exception ex) { + throw new ContextLoadException(context, ex); + } } /** @@ -189,25 +195,30 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader validateMergedContextConfiguration(mergedConfig); GenericApplicationContext context = createContext(); - ApplicationContext parent = mergedConfig.getParentApplicationContext(); - if (parent != null) { - context.setParent(parent); + try { + ApplicationContext parent = mergedConfig.getParentApplicationContext(); + if (parent != null) { + context.setParent(parent); + } + + prepareContext(context); + prepareContext(context, mergedConfig); + customizeBeanFactory(context.getDefaultListableBeanFactory()); + loadBeanDefinitions(context, mergedConfig); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + customizeContext(context); + customizeContext(context, mergedConfig); + + if (!forAotProcessing) { + context.refresh(); + context.registerShutdownHook(); + } + + return context; } - - prepareContext(context); - prepareContext(context, mergedConfig); - customizeBeanFactory(context.getDefaultListableBeanFactory()); - loadBeanDefinitions(context, mergedConfig); - AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - customizeContext(context); - customizeContext(context, mergedConfig); - - if (!forAotProcessing) { - context.refresh(); - context.registerShutdownHook(); + catch (Exception ex) { + throw new ContextLoadException(context, ex); } - - return context; } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 8a1ddc5a276..6aedc74a0a1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -35,6 +35,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; import org.springframework.lang.Nullable; +import org.springframework.test.context.ApplicationContextFailureProcessor; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.ContextConfiguration; @@ -494,15 +495,40 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot /** * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent * interaction with the {@code ContextCache}. - *

The default implementation simply delegates to - * {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}. + *

The default implementation delegates to + * {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()} and + * supplies the returned delegate the configured + * {@link #getApplicationContextFailureProcessor() ApplicationContextFailureProcessor}. *

Concrete subclasses may choose to override this method to return a custom * {@code CacheAwareContextLoaderDelegate} implementation with custom * {@link org.springframework.test.context.cache.ContextCache ContextCache} support. * @return the context loader delegate (never {@code null}) + * @see #getApplicationContextFailureProcessor() */ protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() { - return getBootstrapContext().getCacheAwareContextLoaderDelegate(); + CacheAwareContextLoaderDelegate delegate = getBootstrapContext().getCacheAwareContextLoaderDelegate(); + ApplicationContextFailureProcessor contextFailureProcessor = getApplicationContextFailureProcessor(); + if (contextFailureProcessor != null) { + delegate.setContextFailureProcessor(contextFailureProcessor); + } + return delegate; + } + + /** + * Get the {@link ApplicationContextFailureProcessor} to use. + *

The default implementation returns {@code null}. + *

Concrete subclasses may choose to override this method to provide an + * {@code ApplicationContextFailureProcessor} that will be supplied to the + * configured {@code CacheAwareContextLoaderDelegate} in + * {@link #getCacheAwareContextLoaderDelegate()}. + * @return the context failure processor to use, or {@code null} if no processor + * should be used + * @since 6.0 + * @see #getCacheAwareContextLoaderDelegate() + */ + @Nullable + protected ApplicationContextFailureProcessor getApplicationContextFailureProcessor() { + return null; } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java index 7153e19e20e..98662fe07ab 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java @@ -29,6 +29,7 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.aot.AotContextLoader; import org.springframework.test.context.support.AbstractContextLoader; @@ -156,12 +157,17 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa validateMergedContextConfiguration(webMergedConfig); GenericWebApplicationContext context = createContext(); - configureWebResources(context, webMergedConfig); - prepareContext(context, webMergedConfig); - initializer.initialize(context); - customizeContext(context, webMergedConfig); - context.refresh(); - return context; + try { + configureWebResources(context, webMergedConfig); + prepareContext(context, webMergedConfig); + initializer.initialize(context); + customizeContext(context, webMergedConfig); + context.refresh(); + return context; + } + catch (Exception ex) { + throw new ContextLoadException(context, ex); + } } /** @@ -194,24 +200,28 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa validateMergedContextConfiguration(webMergedConfig); GenericWebApplicationContext context = createContext(); + try { + ApplicationContext parent = mergedConfig.getParentApplicationContext(); + if (parent != null) { + context.setParent(parent); + } + configureWebResources(context, webMergedConfig); + prepareContext(context, webMergedConfig); + customizeBeanFactory(context.getDefaultListableBeanFactory(), webMergedConfig); + loadBeanDefinitions(context, webMergedConfig); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + customizeContext(context, webMergedConfig); + + if (!forAotProcessing) { + context.refresh(); + context.registerShutdownHook(); + } - ApplicationContext parent = mergedConfig.getParentApplicationContext(); - if (parent != null) { - context.setParent(parent); + return context; } - configureWebResources(context, webMergedConfig); - prepareContext(context, webMergedConfig); - customizeBeanFactory(context.getDefaultListableBeanFactory(), webMergedConfig); - loadBeanDefinitions(context, webMergedConfig); - AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - customizeContext(context, webMergedConfig); - - if (!forAotProcessing) { - context.refresh(); - context.registerShutdownHook(); + catch (Exception ex) { + throw new ContextLoadException(context, ex); } - - return context; } /** diff --git a/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java b/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java new file mode 100644 index 00000000000..bb40c7cf348 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-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.test.context.failures; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ApplicationContextFailureProcessor; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.junit.jupiter.FailingTestCase; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.support.DefaultTestContextBootstrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * Tests for failures that occur while loading an {@link ApplicationContext}. + * + * @author Sam Brannen + * @since 6.0 + */ +class ContextLoadFailureTests { + + static List loadFailures = new ArrayList<>(); + + + @BeforeEach + @AfterEach + void clearFailures() { + loadFailures.clear(); + } + + @Test + void customBootstrapperAppliesApplicationContextFailureProcessor() { + assertThat(loadFailures).isEmpty(); + + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(ExplosiveContextTestCase.class))// + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)); + + assertThat(loadFailures).hasSize(1); + LoadFailure loadFailure = loadFailures.get(0); + assertThat(loadFailure.context()).isExactlyInstanceOf(GenericApplicationContext.class); + assertThat(loadFailure.exception()) + .isInstanceOf(BeanCreationException.class) + .cause().isInstanceOf(BeanInstantiationException.class) + .rootCause().isInstanceOf(StackOverflowError.class).hasMessage("Boom!"); + } + + + @FailingTestCase + @SpringJUnitConfig + @BootstrapWith(CustomTestContextBootstrapper.class) + static class ExplosiveContextTestCase { + + @Test + void test1() { + /* no-op */ + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String explosion() { + throw new StackOverflowError("Boom!"); + } + } + } + + static class CustomTestContextBootstrapper extends DefaultTestContextBootstrapper { + + @Override + protected ApplicationContextFailureProcessor getApplicationContextFailureProcessor() { + return (context, exception) -> loadFailures.add(new LoadFailure(context, exception)); + } + } + + record LoadFailure(ApplicationContext context, Throwable exception) {} + +}