美文网首页Java
springboot单元测试Unable to find a @

springboot单元测试Unable to find a @

作者: 提米锅锅 | 来源:发表于2019-10-15 14:56 被阅读0次

    springboot单元测试大部分情况很简单,只用增加2个注解就行:

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest
    

    注意是大部分情况,因为springboot约定大于配置,如果你不按它的约定,就会出现下面的错误
    Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

    这个是项目结构:


    image.png

    Service1Application是springboot启动类。

    我们先分析问题,单元测试要加载spring环境,必须找到main里面的spring启动类Service1Application,出问题的原因就在于自动配置机制找不到这个类,从而无法加载spring环境。

    定位问题从异常堆栈入手,然后增加断点调试,这个是基本的套路。

    at org.springframework.util.Assert.state(Assert.java:73)
    at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.getOrFindConfigurationClasses(SpringBootTestContextBootstrapper.java:240)
    at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.processMergedContextConfiguration(SpringBootTestContextBootstrapper.java:153)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:395)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildDefaultMergedContextConfiguration(AbstractTestContextBootstrapper.java:312)
    

    很容易看到错误是在SpringBootTestContextBootstrappe的getOrFindConfigurationClasses中,findFromClass这个函数找不到用于启动的配置类,需要去看findFromClass到底是怎么去找的。

    protected Class<?>[] getOrFindConfigurationClasses(
                MergedContextConfiguration mergedConfig) {
            Class<?>[] classes = mergedConfig.getClasses();
            if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
                return classes;
            }
            Class<?> found = new SpringBootConfigurationFinder()
                    .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);
        }
    

    findFromClass是个空壳方法,不得不顺着调用链往下找~~~

        public Class<?> findFromClass(Class<?> source) {
            Assert.notNull(source, "Source must not be null");
            return findFromPackage(ClassUtils.getPackageName(source));
        }
    
    private Class<?> scanPackage(String source) {
            while (!source.isEmpty()) {
                Set<BeanDefinition> components = this.scanner.findCandidateComponents(source);
                if (!components.isEmpty()) {
                    Assert.state(components.size() == 1,
                            () -> "Found multiple @SpringBootConfiguration annotated classes "
                                    + components);
                    return ClassUtils.resolveClassName(
                            components.iterator().next().getBeanClassName(), null);
                }
                source = getParentPackage(source);
            }
            return null;
        }
    
    

    这个有点样子了,但是类内容不多,看来干货在scanCandidateComponents。

    这个函数前3行代码是关键,packageSearchPath用测试类的basePackage作为后缀,前面加上了classpath, 也就是 classpath:com/test/example/demo/myservice/*.class,那么它会去所有classpath下的这个路径去找配置类。

    private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
            Set<BeanDefinition> candidates = new LinkedHashSet<>();
            try {
                String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                        resolveBasePackage(basePackage) + '/' + this.resourcePattern;
                Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
                boolean traceEnabled = logger.isTraceEnabled();
                boolean debugEnabled = logger.isDebugEnabled();
                for (Resource resource : resources) {
                    if (traceEnabled) {
                        logger.trace("Scanning " + resource);
                    }
                    if (resource.isReadable()) {
                        try {
                            MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                            if (isCandidateComponent(metadataReader)) {
                                ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                                sbd.setResource(resource);
                                sbd.setSource(resource);
    

    到这里终于发现原来我的测试包路径是com.test.example.demo.myservice,而我的主程序包路径是com.example.demo.myservice,中间多了一个test,导致找不到。


    image.png

    spring单元测试指定配置类的第二种方式。
    问题到了这里并没有完,网上也有人说即使测试的package和代码package不一样,通过指定@SpringBootTest(classes = {Service1Application.class})也可以。

    我们修改程序测试下

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = {Service1Application.class})
    public class testRedis {
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
        @Test
        public  void  run(){
        }
    }
    

    结果真的可以运行,所有我们继续看看这种方式又是如何找到配置类的。

    虽然我们不了解springtest调用链,但知道既然是springboot项目,那必然会创建SpringApplication对象,所有可以在SpringApplication的构造函函数打个断点开始。


    image.png

    org.springframework.boot.test.context的SpringBootContextLoader看起来是测试框架里负责初始化SpringApplication的地方,代码可以看出这个函数自己new了一个SpringApplication 对象,然后根据传入的参数MergedContextConfiguration 对SpringApplication做了初始化。

    @Override
        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());
            }
    
    
    image.png

    spring单元测试其实并不会执行我们源代码的main函数,但是为了模拟程序的环境,它必须拿到Service1Application这个类上配置的全部注解信息。

    再看下config类的信息可以发现Service1Application已经在里面,所以需要继续往前找到config里classes的值是怎么被赋上去的。

    因为调用链很长,我们可以用2分法来加快速度。

        @Override
        public TestContext buildTestContext() {
            return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
                    getCacheAwareContextLoaderDelegate());
        }
    

    直接断到AbstractTestContextBootstrapper的buildTestContext,这个类看起来是准备测试环境的,看一下什么都还没创建,可以直接往下走。

        @Override
        public TestContext buildTestContext() {
            return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
                    getCacheAwareContextLoaderDelegate());
        }
    

    看名字应该是buildMergedContextConfiguration

    image.png

    函数里new了一个defaultConfigAttributesList 对象,看名字猜测就是这是这个对象用于保存配置信息,此时classes属性还没有值,说明springapplication类还没被解析。


    image.png
    private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class<?> testClass,
                CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
    
            List<ContextConfigurationAttributes> defaultConfigAttributesList =
                    Collections.singletonList(new ContextConfigurationAttributes(testClass));
    
            ContextLoader contextLoader = resolveContextLoader(testClass, defaultConfigAttributesList);
            if (logger.isInfoEnabled()) {
                logger.info(String.format(
                        "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s], using %s",
                        testClass.getName(), contextLoader.getClass().getSimpleName()));
            }
            return buildMergedContextConfiguration(testClass, defaultConfigAttributesList, null,
                    cacheAwareContextLoaderDelegate, false);
        }
    

    继续跟进到resolveContextLoader函数,resolveContextLoader的入参

    testClass就是单元测试类 testRedis,这个函数会找到解析出testRedis上的注解信息。
    getclass方法用一个SpringBootTest 类型的对象解析出标准的spring配置类结果,见下图。

    @Override
        protected ContextLoader resolveContextLoader(Class<?> testClass,
                List<ContextConfigurationAttributes> configAttributesList) {
            Class<?>[] classes = getClasses(testClass);
            if (!ObjectUtils.isEmpty(classes)) {
                for (ContextConfigurationAttributes configAttributes : configAttributesList) {
                    addConfigAttributesClasses(configAttributes, classes);
                }
            }
            return super.resolveContextLoader(testClass, configAttributesList);
        }
    
    protected Class<?>[] getClasses(Class<?> testClass) {
            SpringBootTest annotation = getAnnotation(testClass);
            return (annotation != null ? annotation.classes() : null);
        }
    

    SpringBootTest类型对象annotation 已经提取到了配置类


    image.png

    然后会把Service1Application塞到configAttributesList的class属性中,而在buildMergedContextConfiguration里,又会根据configAttributesList来构造一个MergedContextConfiguration对象,最终将这个MergedContextConfiguration 一步一步往下传到loadContext函数,用于初始化SpringApplication对象。

    删减部分无关代码

    private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
                List<ContextConfigurationAttributes> configAttributesList, @Nullable MergedContextConfiguration parentConfig,
                CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
                boolean requireLocationsClassesOrInitializers) {
    
            
            Set<ContextCustomizer> contextCustomizers = getContextCustomizers(testClass,
                    Collections.unmodifiableList(configAttributesList));
    
            MergedTestPropertySources mergedTestPropertySources =
                    TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
            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);
        }
    

    相关文章

      网友评论

        本文标题:springboot单元测试Unable to find a @

        本文链接:https://www.haomeiwen.com/subject/aqojmctx.html