Skip to content

Commit 7dc99a0

Browse files
committed
Add SpringBootTest.useMainMethod support
Add a new `useMainMethod` attribute to `SpringBootTest` which can be used to determine how the test should run. The three available options are: - `ALWAYS` - `NEVER` - `WHEN_AVAILABLE` The default is `WHEN_AVAILABLE` which will attempt to launch the test using the `main` method if there is one. The `SpringBootContextLoader` has been updated to use the new `SpringApplicationHook` interface when the main method is being used. Closes spring-projectsgh-22405
1 parent 57598f1 commit 7dc99a0

File tree

10 files changed

+350
-174
lines changed

10 files changed

+350
-174
lines changed

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@
1616

1717
package org.springframework.boot.test.context;
1818

19+
import java.lang.reflect.Method;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.List;
2223

2324
import org.springframework.beans.BeanUtils;
2425
import org.springframework.boot.ApplicationContextFactory;
26+
import org.springframework.boot.ConfigurableBootstrapContext;
2527
import org.springframework.boot.SpringApplication;
28+
import org.springframework.boot.SpringApplicationHook;
29+
import org.springframework.boot.SpringApplicationRunListener;
30+
import org.springframework.boot.SpringBootConfiguration;
2631
import org.springframework.boot.WebApplicationType;
2732
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
28-
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
33+
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
2934
import org.springframework.boot.test.mock.web.SpringBootMockServletContext;
3035
import org.springframework.boot.test.util.TestPropertyValues;
3136
import org.springframework.boot.test.util.TestPropertyValues.Type;
@@ -54,7 +59,9 @@
5459
import org.springframework.test.context.web.WebMergedContextConfiguration;
5560
import org.springframework.util.Assert;
5661
import org.springframework.util.ObjectUtils;
62+
import org.springframework.util.ReflectionUtils;
5763
import org.springframework.util.StringUtils;
64+
import org.springframework.util.function.ThrowingSupplier;
5865
import org.springframework.web.context.ConfigurableWebApplicationContext;
5966
import org.springframework.web.context.support.GenericWebApplicationContext;
6067

@@ -86,7 +93,47 @@ public class SpringBootContextLoader extends AbstractContextLoader {
8693
@Override
8794
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
8895
assertHasClassesOrLocations(mergedConfig);
96+
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
97+
String[] args = annotation.getArgs();
98+
UseMainMethod useMainMethod = annotation.getUseMainMethod();
99+
ContextLoaderHook hook = new ContextLoaderHook(mergedConfig);
100+
if (useMainMethod != UseMainMethod.NEVER) {
101+
Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
102+
if (mainMethod != null) {
103+
return hook.run(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
104+
}
105+
}
89106
SpringApplication application = getSpringApplication();
107+
return hook.run(() -> application.run(args));
108+
}
109+
110+
private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) {
111+
boolean hasClasses = !ObjectUtils.isEmpty(mergedConfig.getClasses());
112+
boolean hasLocations = !ObjectUtils.isEmpty(mergedConfig.getLocations());
113+
Assert.state(hasClasses || hasLocations,
114+
() -> "No configuration classes or locations found in @SpringApplicationConfiguration. "
115+
+ "For default configuration detection to work you need Spring 4.0.3 or better (found "
116+
+ SpringVersion.getVersion() + ").");
117+
}
118+
119+
private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMethod useMainMethod) {
120+
Class<?> springBootConfiguration = Arrays.stream(mergedConfig.getClasses())
121+
.filter(this::isSpringBootConfiguration).findFirst().orElse(null);
122+
Assert.state(springBootConfiguration != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
123+
"Cannot use main method as no @SpringBootConfiguration-annotated class is available");
124+
Method mainMethod = (springBootConfiguration != null)
125+
? ReflectionUtils.findMethod(springBootConfiguration, "main", String[].class) : null;
126+
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
127+
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
128+
return mainMethod;
129+
}
130+
131+
private boolean isSpringBootConfiguration(Class<?> candidate) {
132+
return MergedAnnotations.from(candidate, SearchStrategy.TYPE_HIERARCHY)
133+
.isPresent(SpringBootConfiguration.class);
134+
}
135+
136+
private void configure(MergedContextConfiguration mergedConfig, SpringApplication application) {
90137
application.setMainApplicationClass(mergedConfig.getTestClass());
91138
application.addPrimarySources(Arrays.asList(mergedConfig.getClasses()));
92139
application.getSources().addAll(Arrays.asList(mergedConfig.getLocations()));
@@ -103,7 +150,8 @@ else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) {
103150
else {
104151
application.setWebApplicationType(WebApplicationType.NONE);
105152
}
106-
application.setApplicationContextFactory((type) -> getApplicationContextFactory(mergedConfig, type));
153+
application.setApplicationContextFactory(
154+
(webApplicationType) -> getApplicationContextFactory(mergedConfig, webApplicationType));
107155
application.setInitializers(initializers);
108156
ConfigurableEnvironment environment = getEnvironment();
109157
if (environment != null) {
@@ -113,30 +161,19 @@ else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) {
113161
else {
114162
application.addListeners(new PrepareEnvironmentListener(mergedConfig));
115163
}
116-
String[] args = SpringBootTestArgs.get(mergedConfig.getContextCustomizers());
117-
return application.run(args);
118-
}
119-
120-
private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) {
121-
boolean hasClasses = !ObjectUtils.isEmpty(mergedConfig.getClasses());
122-
boolean hasLocations = !ObjectUtils.isEmpty(mergedConfig.getLocations());
123-
Assert.state(hasClasses || hasLocations,
124-
() -> "No configuration classes or locations found in @SpringApplicationConfiguration. "
125-
+ "For default configuration detection to work you need Spring 4.0.3 or better (found "
126-
+ SpringVersion.getVersion() + ").");
127164
}
128165

129166
private ConfigurableApplicationContext getApplicationContextFactory(MergedContextConfiguration mergedConfig,
130-
WebApplicationType type) {
131-
if (type != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) {
132-
if (type == WebApplicationType.REACTIVE) {
167+
WebApplicationType webApplicationType) {
168+
if (webApplicationType != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) {
169+
if (webApplicationType == WebApplicationType.REACTIVE) {
133170
return new GenericReactiveWebApplicationContext();
134171
}
135-
if (type == WebApplicationType.SERVLET) {
172+
if (webApplicationType == WebApplicationType.SERVLET) {
136173
return new GenericWebApplicationContext();
137174
}
138175
}
139-
return ApplicationContextFactory.DEFAULT.create(type);
176+
return ApplicationContextFactory.DEFAULT.create(webApplicationType);
140177
}
141178

142179
private void prepareEnvironment(MergedContextConfiguration mergedConfig, SpringApplication application,
@@ -165,9 +202,10 @@ private void setActiveProfiles(ConfigurableEnvironment environment, String[] pro
165202
}
166203

167204
/**
168-
* Builds new {@link org.springframework.boot.SpringApplication} instance. You can
169-
* override this method to add custom behavior
170-
* @return {@link org.springframework.boot.SpringApplication} instance
205+
* Builds new {@link org.springframework.boot.SpringApplication} instance. This method
206+
* is only called when a {@code main} method isn't being used to create the
207+
* {@link SpringApplication}.
208+
* @return a {@link SpringApplication} instance
171209
*/
172210
protected SpringApplication getSpringApplication() {
173211
return new SpringApplication();
@@ -215,16 +253,14 @@ protected List<ApplicationContextInitializer<?>> getInitializers(MergedContextCo
215253
initializers.add(BeanUtils.instantiateClass(initializerClass));
216254
}
217255
if (mergedConfig.getParent() != null) {
218-
initializers
219-
.add(new ParentContextApplicationContextInitializer(mergedConfig.getParentApplicationContext()));
256+
ApplicationContext parentApplicationContext = mergedConfig.getParentApplicationContext();
257+
initializers.add(new ParentContextApplicationContextInitializer(parentApplicationContext));
220258
}
221259
return initializers;
222260
}
223261

224262
private boolean isEmbeddedWebEnvironment(MergedContextConfiguration mergedConfig) {
225-
return MergedAnnotations.from(mergedConfig.getTestClass(), SearchStrategy.TYPE_HIERARCHY)
226-
.get(SpringBootTest.class).getValue("webEnvironment", WebEnvironment.class).orElse(WebEnvironment.NONE)
227-
.isEmbedded();
263+
return SpringBootTestAnnotation.get(mergedConfig).getWebEnvironment().isEmbedded();
228264
}
229265

230266
@Override
@@ -371,4 +407,45 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
371407

372408
}
373409

410+
/**
411+
* {@link SpringApplicationHook} used to capture the {@link ApplicationContext} and to
412+
* trigger early exit for the {@link Mode#AOT_PROCESSING} mode.
413+
*/
414+
private class ContextLoaderHook implements SpringApplicationHook {
415+
416+
private final MergedContextConfiguration mergedConfig;
417+
418+
private ApplicationContext applicationContext;
419+
420+
ContextLoaderHook(MergedContextConfiguration mergedConfig) {
421+
this.mergedConfig = mergedConfig;
422+
}
423+
424+
@Override
425+
public SpringApplicationRunListener getRunListener(SpringApplication application) {
426+
return new SpringApplicationRunListener() {
427+
428+
@Override
429+
public void starting(ConfigurableBootstrapContext bootstrapContext) {
430+
SpringBootContextLoader.this.configure(ContextLoaderHook.this.mergedConfig, application);
431+
}
432+
433+
@Override
434+
public void contextLoaded(ConfigurableApplicationContext context) {
435+
Assert.state(ContextLoaderHook.this.applicationContext == null,
436+
"ApplicationContext already loaded");
437+
ContextLoaderHook.this.applicationContext = context;
438+
}
439+
440+
};
441+
}
442+
443+
private <T> ApplicationContext run(ThrowingSupplier<T> action) {
444+
SpringApplication.withHook(this, action);
445+
Assert.state(this.applicationContext != null, "ApplicationContext not loaded");
446+
return this.applicationContext;
447+
}
448+
449+
}
450+
374451
}

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@
125125
*/
126126
WebEnvironment webEnvironment() default WebEnvironment.MOCK;
127127

128+
/**
129+
* The type of main method usage to employ when creating the {@link SpringApplication}
130+
* under test.
131+
* @return the type of main method usage
132+
* @since 3.0.0
133+
*/
134+
UseMainMethod useMainMethod() default UseMainMethod.WHEN_AVAILABLE;
135+
128136
/**
129137
* An enumeration web environment modes.
130138
*/
@@ -175,4 +183,34 @@ public boolean isEmbedded() {
175183

176184
}
177185

186+
/**
187+
* Enumeration of how the main method of the
188+
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class is used
189+
* when creating and running the {@link SpringApplication} under test.
190+
*/
191+
enum UseMainMethod {
192+
193+
/**
194+
* Always use the {@code main} method. A failure will occur if there is no
195+
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class or
196+
* that class does not have a main method.
197+
*/
198+
ALWAYS,
199+
200+
/**
201+
* Never use the {@code main} method, creating a test-specific
202+
* {@link SpringApplication} instead.
203+
*/
204+
NEVER,
205+
206+
/**
207+
* Use the {@code main} method when it is available. If there is no
208+
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class or
209+
* that class does not have a main method, a test-specific
210+
* {@link SpringApplication} will be used.
211+
*/
212+
WHEN_AVAILABLE;
213+
214+
}
215+
178216
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.context;
18+
19+
import java.util.Arrays;
20+
import java.util.Objects;
21+
22+
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
23+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
24+
import org.springframework.context.ConfigurableApplicationContext;
25+
import org.springframework.test.context.ContextCustomizer;
26+
import org.springframework.test.context.MergedContextConfiguration;
27+
import org.springframework.test.context.TestContextAnnotationUtils;
28+
29+
/**
30+
* {@link ContextCustomizer} to track attributes of
31+
* {@link SpringBootTest @SptringBootTest} that are taken into account when evaluating a
32+
* {@link MergedContextConfiguration} to determine if a context can be shared between
33+
* tests.
34+
*
35+
* @author Phillip Webb
36+
* @author Madhura Bhave
37+
* @author Andy Wilkinson
38+
*/
39+
class SpringBootTestAnnotation implements ContextCustomizer {
40+
41+
private static final String[] NO_ARGS = new String[0];
42+
43+
private static final SpringBootTestAnnotation DEFAULT = new SpringBootTestAnnotation((SpringBootTest) null);
44+
45+
private final String[] args;
46+
47+
private final WebEnvironment webEnvironment;
48+
49+
private final UseMainMethod useMainMethod;
50+
51+
SpringBootTestAnnotation(Class<?> testClass) {
52+
this(TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class));
53+
}
54+
55+
private SpringBootTestAnnotation(SpringBootTest annotation) {
56+
this.args = (annotation != null) ? annotation.args() : NO_ARGS;
57+
this.webEnvironment = (annotation != null) ? annotation.webEnvironment() : WebEnvironment.NONE;
58+
this.useMainMethod = (annotation != null) ? annotation.useMainMethod() : UseMainMethod.WHEN_AVAILABLE;
59+
}
60+
61+
@Override
62+
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
63+
}
64+
65+
@Override
66+
public boolean equals(Object obj) {
67+
if (this == obj) {
68+
return true;
69+
}
70+
if (obj == null || getClass() != obj.getClass()) {
71+
return false;
72+
}
73+
SpringBootTestAnnotation other = (SpringBootTestAnnotation) obj;
74+
boolean result = Arrays.equals(this.args, other.args);
75+
result = result && this.useMainMethod == other.useMainMethod;
76+
result = result && this.webEnvironment == other.webEnvironment;
77+
return result;
78+
}
79+
80+
@Override
81+
public int hashCode() {
82+
final int prime = 31;
83+
int result = 1;
84+
result = prime * result + Arrays.hashCode(this.args);
85+
result = prime * result + Objects.hash(this.useMainMethod, this.webEnvironment);
86+
return result;
87+
}
88+
89+
String[] getArgs() {
90+
return this.args;
91+
}
92+
93+
WebEnvironment getWebEnvironment() {
94+
return this.webEnvironment;
95+
}
96+
97+
UseMainMethod getUseMainMethod() {
98+
return this.useMainMethod;
99+
}
100+
101+
/**
102+
* Return the application arguments from the given {@link MergedContextConfiguration}.
103+
* @param mergedConfig the merged config to check
104+
* @return a {@link SpringBootTestAnnotation} instance
105+
*/
106+
static SpringBootTestAnnotation get(MergedContextConfiguration mergedConfig) {
107+
for (ContextCustomizer customizer : mergedConfig.getContextCustomizers()) {
108+
if (customizer instanceof SpringBootTestAnnotation annotation) {
109+
return annotation;
110+
}
111+
}
112+
return DEFAULT;
113+
}
114+
115+
}

0 commit comments

Comments
 (0)