Springboot Test注解
下面是一个典型的Springboot test的class写法:
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("Test")
public class TaskOperationControllerTest {
...
}
里面涉及到了三个非常常见的test相关的注解:@RunWith(SpringRunner.class),@SpringbootTest,@ActiveProfiles,下面详细的介绍一下这仨个注解的作用。
@RunWith(SpringRunner.class)
SpringJUnit4ClassRunner 的子类,负责在Junit run之前为Test准备Springboot的support,创建context,负责在跑JUnit test之前把Springboot 启动起来。下面就是SpringJUnit4ClassRunner的构造方法,从创建TestContextManager开始。
public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
super(clazz);
if (logger.isDebugEnabled()) {
logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
}
ensureSpringRulesAreNotPresent(clazz);
this.testContextManager = createTestContextManager(clazz);
}
/**
* Create a new {@link TestContextManager} for the supplied test class.
* <p>Can be overridden by subclasses.
* @param clazz the test class to be managed
*/
protected TestContextManager createTestContextManager(Class<?> clazz) {
return new TestContextManager(clazz);
}
在创建TestContextManager时,需要获取TestContextBootstrapper 从而创建Test Context,于是下面的code很明显在当前的Test 类上找@BootstrapWith这个annotation里定义的bootstrapper。
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
if (annotations.isEmpty()) {
return null;
}
if (annotations.size() == 1) {
return annotations.iterator().next().value();
}
// Allow directly-present annotation to override annotations that are meta-present.
BootstrapWith bootstrapWith = testClass.getDeclaredAnnotation(BootstrapWith.class);
if (bootstrapWith != null) {
return bootstrapWith.value();
}
throw new IllegalStateException(String.format(
"Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s",
testClass.getName(), annotations));
}
可是我们的test class上并没有加@BootstrapWith,Springboot Test怎么能找获取TestContextBootstrapper呢,这就涉及到了接下来的这个annotation @SpringBootTest。
@SpringBootTest
看看@SpringbootTest的定义,发现它明确定义了@BootstrapWith
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
...
}
SpringBootTestContextBootstrapper就是context的bootstrapper。SpringJUnit4ClassRunner 会调用SpringBootTestContextBootstrapper#buildTestContext方法用来创建test context,在创建过程中比较核心的是下面这个方法
public final MergedContextConfiguration buildMergedContextConfiguration() {
Class<?> testClass = getBootstrapContext().getTestClass();
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(
testClass, ContextConfiguration.class, ContextHierarchy.class) == null) {
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
}
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
Map<String, List<ContextConfigurationAttributes>> hierarchyMap =
ContextLoaderUtils.buildContextHierarchyMap(testClass);
MergedContextConfiguration parentConfig = null;
MergedContextConfiguration mergedConfig = null;
for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) {
List<ContextConfigurationAttributes> reversedList = new ArrayList<>(list);
Collections.reverse(reversedList);
// Don't use the supplied testClass; instead ensure that we are
// building the MCC for the actual test class that declared the
// configuration for the current level in the context hierarchy.
Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
Class<?> declaringClass = reversedList.get(0).getDeclaringClass();
mergedConfig = buildMergedContextConfiguration(
declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true);
parentConfig = mergedConfig;
}
// Return the last level in the context hierarchy
Assert.state(mergedConfig != null, "No merged context configuration");
return mergedConfig;
}
else {
return buildMergedContextConfiguration(testClass,
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
null, cacheAwareContextLoaderDelegate, true);
}
}
当你的test class中声明了 @ContextConfiguration,那么只有声明的configuration会被使用,不然的话会从test class所在的package一直往上追述,直到找到定义@SpringBootConfiguration的class,也就是@SpringBootApplication标注的Springboot的入口方法(main)的类,
protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
Class<?>[] classes = getOrFindConfigurationClasses(mergedConfig);
List<String> propertySourceProperties = getAndProcessPropertySourceProperties(mergedConfig);
mergedConfig = createModifiedConfig(mergedConfig, classes, StringUtils.toStringArray(propertySourceProperties));
WebEnvironment webEnvironment = getWebEnvironment(mergedConfig.getTestClass());
if (webEnvironment != null && isWebEnvironmentSupported(mergedConfig)) {
WebApplicationType webApplicationType = getWebApplicationType(mergedConfig);
if (webApplicationType == WebApplicationType.SERVLET
&& (webEnvironment.isEmbedded() || webEnvironment == WebEnvironment.MOCK)) {
mergedConfig = new WebMergedContextConfiguration(mergedConfig, determineResourceBasePath(mergedConfig));
}
else if (webApplicationType == WebApplicationType.REACTIVE
&& (webEnvironment.isEmbedded() || webEnvironment == WebEnvironment.MOCK)) {
return new ReactiveWebMergedContextConfiguration(mergedConfig);
}
}
return mergedConfig;
}
protected Class<?>[] getOrFindConfigurationClasses(MergedContextConfiguration mergedConfig) {
Class<?>[] classes = mergedConfig.getClasses();
if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
return classes;
}
Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class)
.findFromClass(mergedConfig.getTestClass());
Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
+ "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
logger.info("Found @SpringBootConfiguration " + found.getName() + " for test " + mergedConfig.getTestClass());
return merge(found, classes);
}
这样Springboot componet scan机制和auto-configuration机制就得以工作。在Springboot 应用启动时,所有scope内的bean都会被加载,这也就是为什么在test class中我们可以Autowire 我们在应用代码中定义的bean的原因。
在build test context的过程中我们还发现了SpringBootTestContextBootstrapper 为我们在config中保存了Active Profile,如下面的code所示:
private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributesList, @Nullable MergedContextConfiguration parentConfig,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
boolean requireLocationsClassesOrInitializers) {
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty");
ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
...
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
StringUtils.toStringArray(locations), ClassUtils.toClassArray(classes),
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
ActiveProfilesUtils.resolveActiveProfiles(testClass),
mergedTestPropertySources.getLocations(),
mergedTestPropertySources.getProperties(),
contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
return processMergedContextConfiguration(mergedConfig);
}
ActiveProfilesUtils.resolveActiveProfiles(testClass) 会获取在Test Class中使用@ActiveProfiles所指定的profile。
@ActiveProfiles
指定Springboot Test需要的Profile,在context load的时候会把保存在config中的profile 直接set到environment中使其生效。可以看到在SpringBootContextLoader中,通过调用setActiveProfiles使得active profile最终set到当前environment中。
public ApplicationContext loadContext(MergedContextConfiguration config) throws Exception {
Class<?>[] configClasses = config.getClasses();
String[] configLocations = config.getLocations();
Assert.state(!ObjectUtils.isEmpty(configClasses) || !ObjectUtils.isEmpty(configLocations),
() -> "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() + ").");
SpringApplication application = getSpringApplication();
application.setMainApplicationClass(config.getTestClass());
application.addPrimarySources(Arrays.asList(configClasses));
application.getSources().addAll(Arrays.asList(configLocations));
ConfigurableEnvironment environment = getEnvironment();
if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
setActiveProfiles(environment, config.getActiveProfiles());
}
ResourceLoader resourceLoader = (application.getResourceLoader() != null) ? application.getResourceLoader()
: new DefaultResourceLoader(null);
TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, resourceLoader,
config.getPropertySourceLocations());
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, getInlinedProperties(config));
application.setEnvironment(environment);
List<ApplicationContextInitializer<?>> initializers = getInitializers(config, application);
if (config instanceof WebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.SERVLET);
if (!isEmbeddedWebEnvironment(config)) {
new WebConfigurer().configure(config, application, initializers);
}
}
else if (config instanceof ReactiveWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.REACTIVE);
if (!isEmbeddedWebEnvironment(config)) {
new ReactiveWebConfigurer().configure(application);
}
}
else {
application.setWebApplicationType(WebApplicationType.NONE);
}
application.setInitializers(initializers);
return application.run(getArgs(config));
}
结合文章“Springboot 读取配置文件原理” 可以知道active profile是怎么发挥作用,怎么能map到对应的application-{profile}.properties/yaml文件的。