From b882de7c68957abd9e60d59bdb76024b0ff732fd Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 8 Nov 2022 16:46:09 -0600 Subject: [PATCH] Throw ContextLoadException on test context load failure When a test context fails to load, a `ContextLoadException` should be thrown so that Framework can catch it and call any registered `ApplicationContextFailureProcessor`s. Closes gh-31793 --- .../test/context/SpringBootContextLoader.java | 45 ++++++--- .../context/SpringBootContextLoaderTests.java | 94 ++++++++++++++++++- .../test/resources/META-INF/spring.factories | 3 + 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index 2043f08b0d6..c4bf0a54dc4 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -55,6 +55,7 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextLoadException; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; @@ -116,7 +117,7 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao } private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode, - ApplicationContextInitializer initializer) { + ApplicationContextInitializer initializer) throws Exception { assertHasClassesOrLocations(mergedConfig); SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig); String[] args = annotation.getArgs(); @@ -125,15 +126,12 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao if (mainMethod != null) { ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, (application) -> configure(mergedConfig, application)); - return hook.run(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args })); + return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args })); } SpringApplication application = getSpringApplication(); configure(mergedConfig, application); - if (mode == Mode.AOT_PROCESSING || mode == Mode.AOT_RUNTIME) { - ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED); - return hook.run(() -> application.run(args)); - } - return application.run(args); + ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED); + return hook.run(() -> application.run(args)); } private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) { @@ -465,10 +463,10 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao } /** - * {@link SpringApplicationHook} used to capture the {@link ApplicationContext} and to - * trigger early exit for the {@link Mode#AOT_PROCESSING} mode. + * {@link SpringApplicationHook} used to capture {@link ApplicationContext} instances + * and to trigger early exit for the {@link Mode#AOT_PROCESSING} mode. */ - private class ContextLoaderHook implements SpringApplicationHook { + private static class ContextLoaderHook implements SpringApplicationHook { private final Mode mode; @@ -478,6 +476,8 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao private final List contexts = Collections.synchronizedList(new ArrayList<>()); + private final List failedContexts = Collections.synchronizedList(new ArrayList<>()); + ContextLoaderHook(Mode mode, ApplicationContextInitializer initializer, Consumer configurer) { this.mode = mode; @@ -506,15 +506,36 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao } } + @Override + public void failed(ConfigurableApplicationContext context, Throwable exception) { + ContextLoaderHook.this.failedContexts.add(context); + } + }; } - private ApplicationContext run(ThrowingSupplier action) { + private ApplicationContext runMain(Runnable action) throws Exception { + return run(() -> { + action.run(); + return null; + }); + } + + private ApplicationContext run(ThrowingSupplier action) throws Exception { try { - SpringApplication.withHook(this, action); + ConfigurableApplicationContext context = SpringApplication.withHook(this, action); + if (context != null) { + return context; + } } catch (AbandonedRunException ex) { } + catch (Exception ex) { + if (this.failedContexts.size() == 1) { + throw new ContextLoadException(this.failedContexts.get(0), ex); + } + throw ex; + } List rootContexts = this.contexts.stream() .filter((context) -> context.getParent() == null).toList(); Assert.state(!rootContexts.isEmpty(), "No root application context located"); diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java index e7e3702e02a..69a5d7ce79b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java @@ -21,20 +21,24 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.test.context.SpringBootTest.UseMainMethod; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ApplicationContextFailureProcessor; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; @@ -55,6 +59,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class SpringBootContextLoaderTests { + @BeforeEach + void setUp() { + ContextLoaderApplicationContextFailureProcessor.reset(); + } + @Test void environmentPropertiesSimple() { Map config = getMergedContextConfigurationProperties(SimpleConfig.class); @@ -197,6 +206,32 @@ class SpringBootContextLoaderTests { assertThat(applicationContext.getEnvironment().getActiveProfiles()).isEmpty(); } + @Test + void whenUseMainMethodWithBeanThrowingException() { + TestContext testContext = new ExposedTestContextManager(UseMainMethodWithBeanThrowingException.class) + .getExposedTestContext(); + assertThatIllegalStateException().isThrownBy(testContext::getApplicationContext).havingCause() + .satisfies((exception) -> { + assertThat(exception).isInstanceOf(BeanCreationException.class); + assertThat(exception) + .isSameAs(ContextLoaderApplicationContextFailureProcessor.contextLoadException); + }); + assertThat(ContextLoaderApplicationContextFailureProcessor.failedContext).isNotNull(); + } + + @Test + void whenNoMainMethodWithBeanThrowingException() { + TestContext testContext = new ExposedTestContextManager(NoMainMethodWithBeanThrowingException.class) + .getExposedTestContext(); + assertThatIllegalStateException().isThrownBy(testContext::getApplicationContext).havingCause() + .satisfies((exception) -> { + assertThat(exception).isInstanceOf(BeanCreationException.class); + assertThat(exception) + .isSameAs(ContextLoaderApplicationContextFailureProcessor.contextLoadException); + }); + assertThat(ContextLoaderApplicationContextFailureProcessor.failedContext).isNotNull(); + } + private String[] getActiveProfiles(Class testClass) { TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext(); ApplicationContext applicationContext = testContext.getApplicationContext(); @@ -284,7 +319,7 @@ class SpringBootContextLoaderTests { } - @SpringBootTest(classes = ConfigWithThrowingMain.class, useMainMethod = UseMainMethod.ALWAYS) + @SpringBootTest(classes = ConfigWithMainThrowingException.class, useMainMethod = UseMainMethod.ALWAYS) static class UseMainMethodAlwaysAndMainMethodThrowsException { } @@ -304,6 +339,16 @@ class SpringBootContextLoaderTests { } + @SpringBootTest(classes = ConfigWithMainWithBeanThrowingException.class, useMainMethod = UseMainMethod.ALWAYS) + static class UseMainMethodWithBeanThrowingException { + + } + + @SpringBootTest(classes = ConfigWithNoMainWithBeanThrowingException.class, useMainMethod = UseMainMethod.NEVER) + static class NoMainMethodWithBeanThrowingException { + + } + @Configuration(proxyBeanMethods = false) static class Config { @@ -327,7 +372,33 @@ class SpringBootContextLoaderTests { @Configuration(proxyBeanMethods = false) @SpringBootConfiguration - public static class ConfigWithThrowingMain { + public static class ConfigWithMainWithBeanThrowingException { + + public static void main(String[] args) { + new SpringApplication(ConfigWithMainWithBeanThrowingException.class).run(); + } + + @Bean + String failContextLoad() { + throw new RuntimeException("ThrownFromBeanMethod"); + } + + } + + @Configuration(proxyBeanMethods = false) + @SpringBootConfiguration + static class ConfigWithNoMainWithBeanThrowingException { + + @Bean + String failContextLoad() { + throw new RuntimeException("ThrownFromBeanMethod"); + } + + } + + @Configuration(proxyBeanMethods = false) + @SpringBootConfiguration + public static class ConfigWithMainThrowingException { public static void main(String[] args) { throw new RuntimeException("ThrownFromMain"); @@ -350,4 +421,23 @@ class SpringBootContextLoaderTests { } + private static class ContextLoaderApplicationContextFailureProcessor implements ApplicationContextFailureProcessor { + + static ApplicationContext failedContext; + + static Throwable contextLoadException; + + @Override + public void processLoadFailure(ApplicationContext context, Throwable exception) { + failedContext = context; + contextLoadException = exception; + } + + private static void reset() { + failedContext = null; + contextLoadException = null; + } + + } + } diff --git a/spring-boot-project/spring-boot-test/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-test/src/test/resources/META-INF/spring.factories index 5eecd4c7e9e..14591fd4edf 100644 --- a/spring-boot-project/spring-boot-test/src/test/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-test/src/test/resources/META-INF/spring.factories @@ -1,2 +1,5 @@ org.springframework.boot.test.context.DefaultTestExecutionListenersPostProcessor=\ org.springframework.boot.test.context.bootstrap.TestDefaultTestExecutionListenersPostProcessor + +org.springframework.test.context.ApplicationContextFailureProcessor=\ +org.springframework.boot.test.context.SpringBootContextLoaderTests.ContextLoaderApplicationContextFailureProcessor \ No newline at end of file