Springboot Test 详解

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文件的。

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容