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
This commit is contained in:
Scott Frederick 2022-11-08 16:46:09 -06:00
parent 70f7258341
commit b882de7c68
3 changed files with 128 additions and 14 deletions

View File

@ -55,6 +55,7 @@ import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextLoadException;
import org.springframework.test.context.ContextLoader; import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader; import org.springframework.test.context.SmartContextLoader;
@ -116,7 +117,7 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
} }
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode, private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer) { ApplicationContextInitializer<ConfigurableApplicationContext> initializer) throws Exception {
assertHasClassesOrLocations(mergedConfig); assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig); SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs(); String[] args = annotation.getArgs();
@ -125,15 +126,12 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
if (mainMethod != null) { if (mainMethod != null) {
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
(application) -> configure(mergedConfig, application)); (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(); SpringApplication application = getSpringApplication();
configure(mergedConfig, application); configure(mergedConfig, application);
if (mode == Mode.AOT_PROCESSING || mode == Mode.AOT_RUNTIME) { ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED); return hook.run(() -> application.run(args));
return hook.run(() -> application.run(args));
}
return application.run(args);
} }
private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) { 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 * {@link SpringApplicationHook} used to capture {@link ApplicationContext} instances
* trigger early exit for the {@link Mode#AOT_PROCESSING} mode. * 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; private final Mode mode;
@ -478,6 +476,8 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
private final List<ApplicationContext> contexts = Collections.synchronizedList(new ArrayList<>()); private final List<ApplicationContext> contexts = Collections.synchronizedList(new ArrayList<>());
private final List<ApplicationContext> failedContexts = Collections.synchronizedList(new ArrayList<>());
ContextLoaderHook(Mode mode, ApplicationContextInitializer<ConfigurableApplicationContext> initializer, ContextLoaderHook(Mode mode, ApplicationContextInitializer<ConfigurableApplicationContext> initializer,
Consumer<SpringApplication> configurer) { Consumer<SpringApplication> configurer) {
this.mode = mode; 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 <T> ApplicationContext run(ThrowingSupplier<T> action) { private <T> ApplicationContext runMain(Runnable action) throws Exception {
return run(() -> {
action.run();
return null;
});
}
private ApplicationContext run(ThrowingSupplier<ConfigurableApplicationContext> action) throws Exception {
try { try {
SpringApplication.withHook(this, action); ConfigurableApplicationContext context = SpringApplication.withHook(this, action);
if (context != null) {
return context;
}
} }
catch (AbandonedRunException ex) { catch (AbandonedRunException ex) {
} }
catch (Exception ex) {
if (this.failedContexts.size() == 1) {
throw new ContextLoadException(this.failedContexts.get(0), ex);
}
throw ex;
}
List<ApplicationContext> rootContexts = this.contexts.stream() List<ApplicationContext> rootContexts = this.contexts.stream()
.filter((context) -> context.getParent() == null).toList(); .filter((context) -> context.getParent() == null).toList();
Assert.state(!rootContexts.isEmpty(), "No root application context located"); Assert.state(!rootContexts.isEmpty(), "No root application context located");

View File

@ -21,20 +21,24 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod; import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ApplicationContextFailureProcessor;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextManager; import org.springframework.test.context.TestContextManager;
@ -55,6 +59,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/ */
class SpringBootContextLoaderTests { class SpringBootContextLoaderTests {
@BeforeEach
void setUp() {
ContextLoaderApplicationContextFailureProcessor.reset();
}
@Test @Test
void environmentPropertiesSimple() { void environmentPropertiesSimple() {
Map<String, Object> config = getMergedContextConfigurationProperties(SimpleConfig.class); Map<String, Object> config = getMergedContextConfigurationProperties(SimpleConfig.class);
@ -197,6 +206,32 @@ class SpringBootContextLoaderTests {
assertThat(applicationContext.getEnvironment().getActiveProfiles()).isEmpty(); 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) { private String[] getActiveProfiles(Class<?> testClass) {
TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext(); TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext(); 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 { 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) @Configuration(proxyBeanMethods = false)
static class Config { static class Config {
@ -327,7 +372,33 @@ class SpringBootContextLoaderTests {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@SpringBootConfiguration @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) { public static void main(String[] args) {
throw new RuntimeException("ThrownFromMain"); 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;
}
}
} }

View File

@ -1,2 +1,5 @@
org.springframework.boot.test.context.DefaultTestExecutionListenersPostProcessor=\ org.springframework.boot.test.context.DefaultTestExecutionListenersPostProcessor=\
org.springframework.boot.test.context.bootstrap.TestDefaultTestExecutionListenersPostProcessor org.springframework.boot.test.context.bootstrap.TestDefaultTestExecutionListenersPostProcessor
org.springframework.test.context.ApplicationContextFailureProcessor=\
org.springframework.boot.test.context.SpringBootContextLoaderTests.ContextLoaderApplicationContextFailureProcessor