详解SpringBootTest运行原理(springbootest)
SpringBootTest运行原理解析
SpringBootTest注解又引用了两个元注解,@ExtendWith和@BootstrapWith。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
}
@ExtendWith是Junit5提供的一个使用其扩展来执行回调的一种方式。 其引用的SpringExtention会在Junit5执行过程中执行回调方法。 然后由SpringExtention驱动SpringBoot初始化。理解整个过程之前,先理解几个关键组件。
@ContextConfiguration和ContextConfigurationAttributes
@ContextConfiguration定义了类级别的元数据,它决定了如何去加载和配置Spring容器。
String[] locations() default {};
Class<?>[] classes() default {};
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
ContextConfigurationAttributes 是用来封装@ContextConfiguration解析后的信息。
public class ContextConfigurationAttributes {
/**
* 空的位置数组常量,用于表示没有配置位置。
*/
private static final String[] EMPTY_LOCATIONS = new String[0];
/**
* 空的类数组常量,用于表示没有配置类。
*/
private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
/**
* 日志记录器,用于记录该类的日志信息。
*/
private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class);
/**
* 声明这些配置属性的类。
* 通常是被@ContextConfiguration注解的测试类。
*/
private final Class<?> declaringClass;
/**
* 配置类数组,用于指定Spring配置类。
*/
private Class<?>[] classes = new Class<?>[0];
/**
* 配置文件位置数组,用于指定XML或其他类型的Spring配置文件的位置。
*/
private String[] locations = new String[0];
/**
* 指示是否应该继承父类的locations配置。
*/
private final boolean inheritLocations;
/**
* ApplicationContext初始化器类数组,用于在context创建后但在加载之前进行自定义初始化。
*/
private final Class<? extends ApplicationContextInitializer<?>>[] initializers;
/**
* 指示是否应该继承父类的initializers配置。
*/
private final boolean inheritInitializers;
/**
* 可选的名称,用于标识特定的配置。
* 可以为null,表示没有指定名称。
*/
@Nullable
private final String name;
/**
* 用于加载ApplicationContext的ContextLoader类。
* 指定如何创建和配置ApplicationContext。
*/
private final Class<? extends ContextLoader> contextLoaderClass;
// ... 构造函数和其他方法 ...
}
MergedContextConfiguration
MergedContextConfiguration封装了一个类上定义的@ContextConfiguration、@ActiveProfiles、@TestPropertySource的信息。
public class MergedContextConfiguration {
/**
* 空字符串数组常量,用于表示没有配置的字符串值。
*/
private static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* 空类数组常量,用于表示没有配置的类。
*/
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
/**
* 空的ApplicationContextInitializer类集合常量,用于表示没有配置的初始化器。
*/
private static final Set<Class<? extends ApplicationContextInitializer<?>>> EMPTY_INITIALIZER_CLASSES =
Collections.emptySet();
/**
* 空的ContextCustomizer集合常量,用于表示没有配置的上下文定制器。
*/
private static final Set<ContextCustomizer> EMPTY_CONTEXT_CUSTOMIZERS = Collections.emptySet();
/**
* 与此配置相关联的测试类。
*/
private final Class<?> testClass;
/**
* Spring配置文件的位置数组。
*/
private final String[] locations;
/**
* 用于配置ApplicationContext的配置类数组。
*/
private final Class<?>[] classes;
/**
* ApplicationContextInitializer类的集合,用于初始化ApplicationContext。
*/
private final Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses;
/**
* 激活的Spring profiles数组。
*/
private final String[] activeProfiles;
/**
* 属性源描述符列表,用于配置测试的属性源。
*/
private final List<PropertySourceDescriptor> propertySourceDescriptors;
/**
* 属性源文件的位置数组。
*/
private final String[] propertySourceLocations;
/**
* 内联属性源属性数组,格式为"key=value"。
*/
private final String[] propertySourceProperties;
/**
* ContextCustomizer的集合,用于自定义ApplicationContext。
*/
private final Set<ContextCustomizer> contextCustomizers;
/**
* 用于加载ApplicationContext的ContextLoader。
*/
private final ContextLoader contextLoader;
/**
* 缓存感知的上下文加载器委托,用于管理ApplicationContext的缓存。
* 可以为null,表示不使用缓存。
*/
@Nullable
private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
/**
* 父MergedContextConfiguration,用于支持层次结构的配置。
* 可以为null,表示没有父配置。
*/
@Nullable
private final MergedContextConfiguration parent;
// ... 构造函数和其他方法 ...
}
MergedContextConfiguration的构建过程
MergedContextConfiguration对象的构建在AbstractTestContextBootstrapper#buildMergedContextConfiguration方法中。
image.png
1、方法开始于对 AbstractTestContextBootstrapper 的 buildMergedContextConfiguration 方法的调用,传入多个参数。
2、首先,调用 resolveContextLoader 方法来确定要使用的 ContextLoader。
3、然后,进入一个循环,遍历 configAttributesList 中的每个 ContextConfigurationAttributes:
如果 contextLoader 是 SmartContextLoader 的实例: 调用 processContextConfiguration 方法 添加位置和类到相应的列表 否则: 调用 processLocations 方法 只添加位置到列表(因为旧版 ContextLoader 不知道如何处理类) 添加初始化器到列表 如果不继承位置,则跳出循环 4、调用 getContextCustomizers 方法获取上下文定制器。
5、使用 TestPropertySourceUtils 构建合并的测试属性源。
6、使用 ApplicationContextInitializerUtils 解析初始化器类。
7、使用 ActiveProfilesUtils 解析激活的配置文件。
8、创建一个新的 MergedContextConfiguration 实例,使用所有收集到的信息。
9、最后,调用 processMergedContextConfiguration 方法处理创建的 MergedContextConfiguration。
10、返回处理后的 MergedContextConfiguration 对象。
TestContextManager和TestContext创建过程
SpringExtention实现了Junit5提供的回调接口
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
ParameterResolver {
public void beforeAll(ExtensionContext context) throws Exception {
// 创建TestContextManager对象,其构造函数内部会创建TestContext实例
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
// 回调所有 TestExecutionListener#beforeTestClass方法
testContextManager.beforeTestClass();
}
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
validateAutowiredConfig(context);
validateRecordApplicationEventsConfig(context);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
// 会回调ServletTestExecutionListener#prepareTestInstance 创建Spring容器
testContextManager.prepareTestInstance(testInstance);
}
}
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
this.testContext = testContextBootstrapper.buildTestContext();
this.testContextHolder = ThreadLocal.withInitial(() -> copyTestContext(this.testContext));
registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
}
// 基于MergedContextConfiguration 创建TestContext实例
public TestContext buildTestContext() {
return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
getCacheAwareContextLoaderDelegate());
}
Spring容器的创建过程
ServletTestExecutionListener#prepareTestInstance方法解析
上面说了ServletTestExecutionListener#prepareTestInstance方法会创建Spring容器。源码如下
private void setUpRequestContextIfNecessary(TestContext testContext) {
if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
return;
}
// 调用TestContext的getApplication方法获取Spring容器实例
ApplicationContext context = testContext.getApplicationContext();
if (context instanceof WebApplicationContext wac) {
ServletContext servletContext = wac.getServletContext();
MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE);
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
RequestContextHolder.setRequestAttributes(servletWebRequest);
testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
if (wac instanceof ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
// 代码中可以获取此处注入的Mock对象依赖
bf.registerResolvableDependency(MockHttpServletResponse.class, response);
bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
}
}
}
TestContext#getApplicationContext方法解析
public ApplicationContext getApplicationContext() {
// 这里会去调用DefaultCacheAwareContextLoaderDelegate#loadContext方法
ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedConfig);
return context;
}
DefaultCacheAwareContextLoaderDelegate#loadContext方法解析
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
mergedConfig = replaceIfNecessary(mergedConfig);
synchronized (this.contextCache) {
ApplicationContext context = this.contextCache.get(mergedConfig);
try {
if (context == null) {
if (mergedConfig instanceof AotMergedContextConfiguration aotMergedConfig) {
context = loadContextInAotMode(aotMergedConfig);
}
else {
// 此处会去调用loadContextInternal方法,传入的参数即前面创建好的MergedContextConfiguration对象
context = loadContextInternal(mergedConfig);
}
this.contextCache.put(mergedConfig, context);
}
}
finally {
this.contextCache.logStatistics();
}
return context;
}
}
protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedConfig)
throws Exception {
// 这里解析出来的是SpringBootContextLoader对象,其实现了SmartContextLoader接口
ContextLoader contextLoader = getContextLoader(mergedConfig);
if (contextLoader instanceof SmartContextLoader smartContextLoader) {
会去调用SpringBootContextLoader#loadContext方法
return smartContextLoader.loadContext(mergedConfig);
}
else {
String[] locations = mergedConfig.getLocations();
return contextLoader.loadContext(locations);
}
}
SpringBootContextLoader#loadContext方法解析
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer) throws Exception {
assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs();
// 判断是否需要去执行Main方法初始化Spring容器
UseMainMethod useMainMethod = annotation.getUseMainMethod();
Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
if (mainMethod != null) {
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
(application) -> configure(mergedConfig, application));
return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
}
// 这里会去调用 new SpringApplication(),创建SpringApplication对象
SpringApplication application = getSpringApplication();
// 这里会去配置Spring 容器
configure(mergedConfig, application);
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
return hook.run(() -> application.run(args));
}
SpringBootContextLoader#configure方法解析
private void configure(MergedContextConfiguration mergedConfig, SpringApplication application) {
// 设置入口类
application.setMainApplicationClass(mergedConfig.getTestClass());
application.addPrimarySources(Arrays.asList(mergedConfig.getClasses()));
application.getSources().addAll(Arrays.asList(mergedConfig.getLocations()));
// 获取ApplicationContextInitializer
List<ApplicationContextInitializer<?>> initializers = getInitializers(mergedConfig, application);
if (mergedConfig instanceof WebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.SERVLET);
if (!isEmbeddedWebEnvironment(mergedConfig)) {
new WebConfigurer().configure(mergedConfig, initializers);
}
}
else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.REACTIVE);
}
else {
application.setWebApplicationType(WebApplicationType.NONE);
}
application.setApplicationContextFactory(getApplicationContextFactory(mergedConfig));
if (mergedConfig.getParent() != null) {
application.setBannerMode(Banner.Mode.OFF);
}
// 设置初始化器,SpringBoot启动过程会回调初始化器
application.setInitializers(initializers);
ConfigurableEnvironment environment = getEnvironment();
if (environment != null) {
prepareEnvironment(mergedConfig, application, environment, false);
application.setEnvironment(environment);
}
else {
// 添加事件监听器,在Spring容器启动过程中会初始化Environment
application.addListeners(new PrepareEnvironmentListener(mergedConfig));
}
}
TestExecutionListener
在测试用例运行过程中,Junit5会回调其提供的回调接口,而SpringExtention实现了多个回调接口,如下
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
ParameterResolver {
public void beforeAll(ExtensionContext context) throws Exception {
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestClass();
}
public void afterAll(ExtensionContext context) throws Exception {
try {
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestClass();
}
finally {
getStore(context).remove(context.getRequiredTestClass());
}
}
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
validateAutowiredConfig(context);
validateRecordApplicationEventsConfig(context);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.prepareTestInstance(testInstance);
}
public void beforeEach(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestMethod(testInstance, testMethod);
}
public void beforeTestExecution(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestExecution(testInstance, testMethod);
}
public void afterTestExecution(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
Throwable testException = context.getExecutionException().orElse(null);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestExecution(testInstance, testMethod, testException);
}
public void afterEach(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
Throwable testException = context.getExecutionException().orElse(null);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestMethod(testInstance, testMethod, testException);
}
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
Executable executable = parameter.getDeclaringExecutable();
Class<?> testClass = extensionContext.getRequiredTestClass();
PropertyProvider junitPropertyProvider = propertyName ->
extensionContext.getConfigurationParameter(propertyName).orElse(null);
return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) ||
ApplicationContext.class.isAssignableFrom(parameter.getType()) ||
supportsApplicationEvents(parameterContext) ||
ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex()));
}
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
int index = parameterContext.getIndex();
Class<?> testClass = extensionContext.getRequiredTestClass();
ApplicationContext applicationContext = getApplicationContext(extensionContext);
return ParameterResolutionDelegate.resolveDependency(parameter, index, testClass,
applicationContext.getAutowireCapableBeanFactory());
}
}
其在整个测试用例执行过程中的执行顺序如下
1、beforeAll(ExtensionContext context)
在所有测试方法执行之前调用,用于整个测试类的设置。
2、postProcessTestInstance(Object testInstance, ExtensionContext context)
在创建测试实例后,但在执行任何测试方法之前调用。 用于处理测试实例,例如注入依赖。
3、beforeEach(ExtensionContext context)
在每个测试方法执行之前调用。 用于设置每个测试方法的环境。
4、beforeTestExecution(ExtensionContext context)
在测试方法执行之前,但在 beforeEach 之后调用。 用于在测试方法执行前进行最后的准备工作。
5、supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) 和resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
这两个方法用于参数解析,在测试方法执行时被调用。 supportsParameter 检查是否支持解析特定参数。 resolveParameter 实际解析并提供参数值。 测试方法执行
6、afterTestExecution(ExtensionContext context)
在测试方法执行之后立即调用。 用于在测试方法执行后立即进行一些清理或验证工作。
7、afterEach(ExtensionContext context)
在每个测试方法执行之后调用。 用于清理每个测试方法的环境。 重复步骤 3-8 对于每个测试方法
8、afterAll(ExtensionContext context)
在所有测试方法执行完毕后调用。 用于整个测试类的清理工作。
常见的TestExecutionListener如下:
org.springframework.test.context.web.ServletTestExecutionListener
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
org.springframework.test.context.event.ApplicationEventsTestExecutionListener
org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener
org.springframework.test.context.support.DependencyInjectionTestExecutionListener
org.springframework.test.context.support.DirtiesContextTestExecutionListener
org.springframework.test.context.event.EventPublishingTestExecutionListener
org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener
org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener
org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener
org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener
org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener
原文:https://juejin.cn/post/7405733837132005430
作者:Truism2