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 gh-22405
pull/32405/head
Phillip Webb 2 years ago
parent fadbb4b763
commit 41e0bbf4bb

@ -137,6 +137,44 @@ Therefore, as long as your tests share the same configuration (no matter how it
[[features.testing.spring-boot-applications.using-main]]
==== Using the Test Configuration Main Method
Typically the test configuration discovered by `@SpringBootTest` will be your main `@SpringBootApplication`.
In most well structured applications, this configuration class will also include the `main` method used to launch the application.
For example, the following is a very common code pattern for a typical Spring Boot application:
include::code:typical/MyApplication[]
In the example above, the `main` method doesn't do anything other than delegate to `SpringApplication.run`.
It is, however, possible to have a more complex `main` method that applies customizations before calling `SpringApplication.run`.
For example, here is an application that changes the banner mode and sets additional profiles:
include::code:custom/MyApplication[]
Since customizations in the `main` method can affect the resulting `ApplicationContext`, Spring Boot will also attempt to use the `main` method for tests.
By default, `@SpringBootTest` will detect any `main` method on your `@SpringBootConfiguration` and run it up to the point that the `SpringApplication.run` method is called.
If your `@SpringBootConfiguration` class doesn't have a main method, the class itself is used directly to create the `ApplicationContext`.
In some situations, you may find that you can't or don't want to run the `main` method in your tests.
If that's the case, you can change the `useMainMethod` attribute of `@SpringBootTest` to `UseMainMethod.NEVER`.
For example, you might have the following application class:
include::code:never/MyApplication[]
If a test wants to use the `MyApplication` configuration without calling the main method, it can be written as follows:
include::code:never/MyApplicationTests[]
The test above will still use `MyApplication` to create the `ApplicationContext`, however, it won't call `MyCode.expensiveOperation()` since the `main` method is not invoked.
If you want to do the opposite, and ensure that the `main` method is always invoked, you can set the `useMainMethod` attribute of `@SpringBootTest` to `UseMainMethod.ALWAYS`.
If this property is set, and no `main` method can be found the test will fail.
[[features.testing.spring-boot-applications.excluding-configuration]]
==== Excluding Test Configuration
If your application uses component scanning (for example, if you use `@SpringBootApplication` or `@ComponentScan`), you may find top-level configuration classes that you created only for specific tests accidentally get picked up everywhere.

@ -0,0 +1,33 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.custom;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.setAdditionalProfiles("myprofile");
application.run(args);
}
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.never;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
MyCode.expensiveOperation();
SpringApplication.run(MyApplication.class, args);
}
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.never;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
@SpringBootTest(useMainMethod = UseMainMethod.NEVER)
public class MyApplicationTests {
@Test
void exampleTest() {
// ...
}
}

@ -0,0 +1,27 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.never;
public final class MyCode {
private MyCode() {
}
static void expensiveOperation() {
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.typical;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.custom
import org.springframework.boot.Banner
import org.springframework.boot.runApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args) {
setBannerMode(Banner.Mode.OFF)
setAdditionalProfiles("myprofile");
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.custom.never
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.docs.using.structuringyourcode.locatingthemainclass.MyApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
MyCode.expensiveOperation()
runApplication<MyApplication>(*args)
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.custom.never
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod
import org.springframework.context.annotation.Import
@SpringBootTest(useMainMethod = UseMainMethod.NEVER)
class MyApplicationTests {
@Test
fun exampleTest() {
// ...
}
}

@ -0,0 +1,24 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.custom.never
class MyCode {
companion object {
fun expensiveOperation() {
}
}
}

@ -0,0 +1,28 @@
/*
* Copyright 2012-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.boot.docs.features.testing.springbootapplications.usingmain.typical
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.docs.using.structuringyourcode.locatingthemainclass.MyApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}

@ -16,16 +16,23 @@
package org.springframework.boot.test.context;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplication.AbandonedRunException;
import org.springframework.boot.SpringApplicationHook;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import org.springframework.boot.test.mock.web.SpringBootMockServletContext;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.util.TestPropertyValues.Type;
@ -54,7 +61,9 @@ import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.context.web.WebMergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.ThrowingSupplier;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
@ -86,7 +95,47 @@ public class SpringBootContextLoader extends AbstractContextLoader {
@Override
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs();
UseMainMethod useMainMethod = annotation.getUseMainMethod();
ContextLoaderHook hook = new ContextLoaderHook(mergedConfig);
if (useMainMethod != UseMainMethod.NEVER) {
Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
if (mainMethod != null) {
return hook.run(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
}
}
SpringApplication application = getSpringApplication();
return hook.run(() -> application.run(args));
}
private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) {
boolean hasClasses = !ObjectUtils.isEmpty(mergedConfig.getClasses());
boolean hasLocations = !ObjectUtils.isEmpty(mergedConfig.getLocations());
Assert.state(hasClasses || hasLocations,
() -> "No configuration classes or locations found in @SpringApplicationConfiguration. "
+ "For default configuration detection to work you need Spring 4.0.3 or better (found "
+ SpringVersion.getVersion() + ").");
}
private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMethod useMainMethod) {
Class<?> springBootConfiguration = Arrays.stream(mergedConfig.getClasses())
.filter(this::isSpringBootConfiguration).findFirst().orElse(null);
Assert.state(springBootConfiguration != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
"Cannot use main method as no @SpringBootConfiguration-annotated class is available");
Method mainMethod = (springBootConfiguration != null)
? ReflectionUtils.findMethod(springBootConfiguration, "main", String[].class) : null;
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
return mainMethod;
}
private boolean isSpringBootConfiguration(Class<?> candidate) {
return MergedAnnotations.from(candidate, SearchStrategy.TYPE_HIERARCHY)
.isPresent(SpringBootConfiguration.class);
}
private void configure(MergedContextConfiguration mergedConfig, SpringApplication application) {
application.setMainApplicationClass(mergedConfig.getTestClass());
application.addPrimarySources(Arrays.asList(mergedConfig.getClasses()));
application.getSources().addAll(Arrays.asList(mergedConfig.getLocations()));
@ -103,7 +152,8 @@ public class SpringBootContextLoader extends AbstractContextLoader {
else {
application.setWebApplicationType(WebApplicationType.NONE);
}
application.setApplicationContextFactory((type) -> getApplicationContextFactory(mergedConfig, type));
application.setApplicationContextFactory(
(webApplicationType) -> getApplicationContextFactory(mergedConfig, webApplicationType));
application.setInitializers(initializers);
ConfigurableEnvironment environment = getEnvironment();
if (environment != null) {
@ -113,30 +163,19 @@ public class SpringBootContextLoader extends AbstractContextLoader {
else {
application.addListeners(new PrepareEnvironmentListener(mergedConfig));
}
String[] args = SpringBootTestArgs.get(mergedConfig.getContextCustomizers());
return application.run(args);
}
private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) {
boolean hasClasses = !ObjectUtils.isEmpty(mergedConfig.getClasses());
boolean hasLocations = !ObjectUtils.isEmpty(mergedConfig.getLocations());
Assert.state(hasClasses || hasLocations,
() -> "No configuration classes or locations found in @SpringApplicationConfiguration. "
+ "For default configuration detection to work you need Spring 4.0.3 or better (found "
+ SpringVersion.getVersion() + ").");
}
private ConfigurableApplicationContext getApplicationContextFactory(MergedContextConfiguration mergedConfig,
WebApplicationType type) {
if (type != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) {
if (type == WebApplicationType.REACTIVE) {
WebApplicationType webApplicationType) {
if (webApplicationType != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) {
if (webApplicationType == WebApplicationType.REACTIVE) {
return new GenericReactiveWebApplicationContext();
}
if (type == WebApplicationType.SERVLET) {
if (webApplicationType == WebApplicationType.SERVLET) {
return new GenericWebApplicationContext();
}
}
return ApplicationContextFactory.DEFAULT.create(type);
return ApplicationContextFactory.DEFAULT.create(webApplicationType);
}
private void prepareEnvironment(MergedContextConfiguration mergedConfig, SpringApplication application,
@ -165,9 +204,10 @@ public class SpringBootContextLoader extends AbstractContextLoader {
}
/**
* Builds new {@link org.springframework.boot.SpringApplication} instance. You can
* override this method to add custom behavior
* @return {@link org.springframework.boot.SpringApplication} instance
* Builds new {@link org.springframework.boot.SpringApplication} instance. This method
* is only called when a {@code main} method isn't being used to create the
* {@link SpringApplication}.
* @return a {@link SpringApplication} instance
*/
protected SpringApplication getSpringApplication() {
return new SpringApplication();
@ -215,16 +255,14 @@ public class SpringBootContextLoader extends AbstractContextLoader {
initializers.add(BeanUtils.instantiateClass(initializerClass));
}
if (mergedConfig.getParent() != null) {
initializers
.add(new ParentContextApplicationContextInitializer(mergedConfig.getParentApplicationContext()));
ApplicationContext parentApplicationContext = mergedConfig.getParentApplicationContext();
initializers.add(new ParentContextApplicationContextInitializer(parentApplicationContext));
}
return initializers;
}
private boolean isEmbeddedWebEnvironment(MergedContextConfiguration mergedConfig) {
return MergedAnnotations.from(mergedConfig.getTestClass(), SearchStrategy.TYPE_HIERARCHY)
.get(SpringBootTest.class).getValue("webEnvironment", WebEnvironment.class).orElse(WebEnvironment.NONE)
.isEmbedded();
return SpringBootTestAnnotation.get(mergedConfig).getWebEnvironment().isEmbedded();
}
@Override
@ -371,4 +409,45 @@ public class SpringBootContextLoader extends AbstractContextLoader {
}
/**
* {@link SpringApplicationHook} used to capture the {@link ApplicationContext} and to
* trigger early exit for the {@link Mode#AOT_PROCESSING} mode.
*/
private class ContextLoaderHook implements SpringApplicationHook {
private final MergedContextConfiguration mergedConfig;
ContextLoaderHook(MergedContextConfiguration mergedConfig) {
this.mergedConfig = mergedConfig;
}
@Override
public SpringApplicationRunListener getRunListener(SpringApplication application) {
return new SpringApplicationRunListener() {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
SpringBootContextLoader.this.configure(ContextLoaderHook.this.mergedConfig, application);
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
throw new AbandonedRunException(context);
}
};
}
private <T> ApplicationContext run(ThrowingSupplier<T> action) {
try {
SpringApplication.withHook(this, action);
throw new IllegalStateException("ApplicationContext not loaded");
}
catch (AbandonedRunException ex) {
return ex.getApplicationContext();
}
}
}
}

@ -125,6 +125,14 @@ public @interface SpringBootTest {
*/
WebEnvironment webEnvironment() default WebEnvironment.MOCK;
/**
* The type of main method usage to employ when creating the {@link SpringApplication}
* under test.
* @return the type of main method usage
* @since 3.0.0
*/
UseMainMethod useMainMethod() default UseMainMethod.WHEN_AVAILABLE;
/**
* An enumeration web environment modes.
*/
@ -175,4 +183,34 @@ public @interface SpringBootTest {
}
/**
* Enumeration of how the main method of the
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class is used
* when creating and running the {@link SpringApplication} under test.
*/
enum UseMainMethod {
/**
* Always use the {@code main} method. A failure will occur if there is no
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class or
* that class does not have a main method.
*/
ALWAYS,
/**
* Never use the {@code main} method, creating a test-specific
* {@link SpringApplication} instead.
*/
NEVER,
/**
* Use the {@code main} method when it is available. If there is no
* {@link SpringBootConfiguration @SpringBootConfiguration}-annotated class or
* that class does not have a main method, a test-specific
* {@link SpringApplication} will be used.
*/
WHEN_AVAILABLE;
}
}

@ -0,0 +1,115 @@
/*
* Copyright 2012-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.boot.test.context;
import java.util.Arrays;
import java.util.Objects;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* {@link ContextCustomizer} to track attributes of
* {@link SpringBootTest @SptringBootTest} that are taken into account when evaluating a
* {@link MergedContextConfiguration} to determine if a context can be shared between
* tests.
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class SpringBootTestAnnotation implements ContextCustomizer {
private static final String[] NO_ARGS = new String[0];
private static final SpringBootTestAnnotation DEFAULT = new SpringBootTestAnnotation((SpringBootTest) null);
private final String[] args;
private final WebEnvironment webEnvironment;
private final UseMainMethod useMainMethod;
SpringBootTestAnnotation(Class<?> testClass) {
this(TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class));
}
private SpringBootTestAnnotation(SpringBootTest annotation) {
this.args = (annotation != null) ? annotation.args() : NO_ARGS;
this.webEnvironment = (annotation != null) ? annotation.webEnvironment() : WebEnvironment.NONE;
this.useMainMethod = (annotation != null) ? annotation.useMainMethod() : UseMainMethod.WHEN_AVAILABLE;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SpringBootTestAnnotation other = (SpringBootTestAnnotation) obj;
boolean result = Arrays.equals(this.args, other.args);
result = result && this.useMainMethod == other.useMainMethod;
result = result && this.webEnvironment == other.webEnvironment;
return result;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(this.args);
result = prime * result + Objects.hash(this.useMainMethod, this.webEnvironment);
return result;
}
String[] getArgs() {
return this.args;
}
WebEnvironment getWebEnvironment() {
return this.webEnvironment;
}
UseMainMethod getUseMainMethod() {
return this.useMainMethod;
}
/**
* Return the application arguments from the given {@link MergedContextConfiguration}.
* @param mergedConfig the merged config to check
* @return a {@link SpringBootTestAnnotation} instance
*/
static SpringBootTestAnnotation get(MergedContextConfiguration mergedConfig) {
for (ContextCustomizer customizer : mergedConfig.getContextCustomizers()) {
if (customizer instanceof SpringBootTestAnnotation annotation) {
return annotation;
}
}
return DEFAULT;
}
}

@ -1,79 +0,0 @@
/*
* Copyright 2012-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.boot.test.context;
import java.util.Arrays;
import java.util.Set;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
/**
* {@link ContextCustomizer} to track application arguments that are used in a
* {@link SpringBootTest}. The application arguments are taken into account when
* evaluating a {@link MergedContextConfiguration} to determine if a context can be shared
* between tests.
*
* @author Madhura Bhave
*/
class SpringBootTestArgs implements ContextCustomizer {
private static final String[] NO_ARGS = new String[0];
private final String[] args;
SpringBootTestArgs(Class<?> testClass) {
this.args = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
.get(SpringBootTest.class).getValue("args", String[].class).orElse(NO_ARGS);
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
}
String[] getArgs() {
return this.args;
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (getClass() == obj.getClass())
&& Arrays.equals(this.args, ((SpringBootTestArgs) obj).args);
}
@Override
public int hashCode() {
return Arrays.hashCode(this.args);
}
/**
* Return the application arguments from the given customizers.
* @param customizers the customizers to check
* @return the application args or an empty array
*/
static String[] get(Set<ContextCustomizer> customizers) {
for (ContextCustomizer customizer : customizers) {
if (customizer instanceof SpringBootTestArgs testArgs) {
return testArgs.args;
}
}
return NO_ARGS;
}
}

@ -362,8 +362,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr
protected final MergedContextConfiguration createModifiedConfig(MergedContextConfiguration mergedConfig,
Class<?>[] classes, String[] propertySourceProperties) {
Set<ContextCustomizer> contextCustomizers = new LinkedHashSet<>(mergedConfig.getContextCustomizers());
contextCustomizers.add(new SpringBootTestArgs(mergedConfig.getTestClass()));
contextCustomizers.add(new SpringBootTestWebEnvironment(mergedConfig.getTestClass()));
contextCustomizers.add(new SpringBootTestAnnotation(mergedConfig.getTestClass()));
return new MergedContextConfiguration(mergedConfig.getTestClass(), mergedConfig.getLocations(), classes,
mergedConfig.getContextInitializerClasses(), mergedConfig.getActiveProfiles(),
mergedConfig.getPropertySourceLocations(), propertySourceProperties, contextCustomizers,

@ -1,58 +0,0 @@
/*
* Copyright 2012-2020 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.boot.test.context;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* {@link ContextCustomizer} to track the web environment that is used in a
* {@link SpringBootTest}. The web environment is taken into account when evaluating a
* {@link MergedContextConfiguration} to determine if a context can be shared between
* tests.
*
* @author Andy Wilkinson
*/
class SpringBootTestWebEnvironment implements ContextCustomizer {
private final WebEnvironment webEnvironment;
SpringBootTestWebEnvironment(Class<?> testClass) {
SpringBootTest sprintBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
SpringBootTest.class);
this.webEnvironment = (sprintBootTest != null) ? sprintBootTest.webEnvironment() : null;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (getClass() == obj.getClass())
&& this.webEnvironment == ((SpringBootTestWebEnvironment) obj).webEnvironment;
}
@Override
public int hashCode() {
return (this.webEnvironment != null) ? this.webEnvironment.hashCode() : 0;
}
}

@ -23,6 +23,9 @@ import java.util.stream.Collectors;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
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;
@ -40,6 +43,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link SpringBootContextLoader}
@ -114,8 +118,7 @@ class SpringBootContextLoaderTests {
assertThat(getActiveProfiles(ActiveProfileWithComma.class)).containsExactly("profile1,2");
}
@Test
// gh-28776
@Test // gh-28776
void testPropertyValuesShouldTakePrecedenceWhenInlinedPropertiesPresent() {
TestContext context = new ExposedTestContextManager(SimpleConfig.class).getExposedTestContext();
StandardEnvironment environment = (StandardEnvironment) context.getApplicationContext().getEnvironment();
@ -162,6 +165,37 @@ class SpringBootContextLoaderTests {
assertThat(context.getApplicationContext()).isInstanceOf(GenericReactiveWebApplicationContext.class);
}
@Test
void whenUseMainMethodAlwaysAndMainMethodThrowsException() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodAlwaysAndMainMethodThrowsException.class)
.getExposedTestContext();
assertThatIllegalStateException().isThrownBy(testContext::getApplicationContext).havingCause()
.withMessageContaining("ThrownFromMain");
}
@Test
void whenUseMainMethodWhenAvailableAndNoMainMethod() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndNoMainMethod.class)
.getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext();
assertThat(applicationContext.getEnvironment().getActiveProfiles()).isEmpty();
}
@Test
void whenUseMainMethodWhenAvailableAndMainMethod() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndMainMethod.class)
.getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext();
assertThat(applicationContext.getEnvironment().getActiveProfiles()).contains("frommain");
}
@Test
void whenUseMainMethodNever() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodNever.class).getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext();
assertThat(applicationContext.getEnvironment().getActiveProfiles()).isEmpty();
}
private String[] getActiveProfiles(Class<?> testClass) {
TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext();
@ -239,18 +273,64 @@ class SpringBootContextLoaderTests {
}
@SpringBootTest(classes = Config.class, args = "--spring.main.web-application-type=none")
static class ChangingWebApplicationTypeToNone {
}
@SpringBootTest(classes = Config.class, args = "--spring.main.web-application-type=reactive")
static class ChangingWebApplicationTypeToReactive {
}
@SpringBootTest(classes = ConfigWithThrowingMain.class, useMainMethod = UseMainMethod.ALWAYS)
static class UseMainMethodAlwaysAndMainMethodThrowsException {
}
@SpringBootTest(classes = ConfigWithNoMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UseMainMethodWhenAvailableAndNoMainMethod {
}
@SpringBootTest(classes = ConfigWithMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UseMainMethodWhenAvailableAndMainMethod {
}
@SpringBootTest(classes = ConfigWithMain.class, useMainMethod = UseMainMethod.NEVER)
static class UseMainMethodNever {
}
@Configuration(proxyBeanMethods = false)
static class Config {
}
@SpringBootTest(classes = Config.class, args = "--spring.main.web-application-type=none")
static class ChangingWebApplicationTypeToNone {
@Configuration(proxyBeanMethods = false)
@SpringBootConfiguration
public static class ConfigWithMain {
public static void main(String[] args) {
new SpringApplication(ConfigWithMain.class).run("--spring.profiles.active=frommain");
}
@SpringBootTest(classes = Config.class, args = "--spring.main.web-application-type=reactive")
static class ChangingWebApplicationTypeToReactive {
}
@Configuration(proxyBeanMethods = false)
@SpringBootConfiguration
static class ConfigWithNoMain {
}
@Configuration(proxyBeanMethods = false)
@SpringBootConfiguration
public static class ConfigWithThrowingMain {
public static void main(String[] args) {
throw new RuntimeException("ThrownFromMain");
}
}

@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
@ -33,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = WebEnvironment.NONE, properties = { "enableEnvironmentPostProcessor=true" }) // gh-28530
@SpringBootTest(useMainMethod = UseMainMethod.NEVER, webEnvironment = WebEnvironment.NONE,
properties = { "enableEnvironmentPostProcessor=true" }) // gh-28530
@ActiveProfiles("hello")
class ActiveProfilesTests {

@ -20,11 +20,12 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import static org.assertj.core.api.Assertions.assertThat;
// gh-29169
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(useMainMethod = UseMainMethod.NEVER, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AttributeInjectionTests {
@Autowired(required = false)

Loading…
Cancel
Save