mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
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:
parent
70f7258341
commit
b882de7c68
@ -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");
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user