(1). 概述

前几天,稍微研究了下Junit,是因为IDEA(Eclipse)有插件,会回调Junit的单元测试,那么:Spring是如何与Junit进行整合的呢?
答案就在:org.springframework.test.context.junit4.SpringRunner里.

(2). SpringApplicationTest

查看简单的测试案例.

// 1. RunWith会被IDEA回调执行
@RunWith(SpringRunner.class)
// 2. SpringRunner内部会通过反射获得当前类(SpringApplicationTest)的所有(注解)信息,并创建Web容器.
@SpringBootTest(classes = {HelloController.class, Conf.class})
public class SpringApplicationTest {

    @Autowired
    private IUserService userService;

    @Test
    public void testHello() {
        System.out.println("*************************testHello");
    }
}

(3). SpringRunner

很简单,直接调用了父类:SpringJUnit4ClassRunner


// 1. 注意是final,所以,我们自己不能继承于,得继承于:SpringJUnit4ClassRunner
public final class SpringRunner 
			 // *****************************************************
			 // 2. SpringJUnit4ClassRunner
			 // *****************************************************
             extends SpringJUnit4ClassRunner {

	// 1. clazz = help.lixin.test.SpringApplicationTest
	public SpringRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
	} // end 
}

(4). SpringJUnit4ClassRunner

package org.springframework.test.context.junit4;

public class SpringJUnit4ClassRunner 
       // ***********************************************************
	   // 1. 在前面分析过:BlockJUnit4ClassRunner是Junit的核心类,会被IDEA或者Maven插件回调.
	   // ***********************************************************
       extends BlockJUnit4ClassRunner {
	
	private final TestContextManager testContextManager;
	
	// clazz = help.lixin.test.SpringApplicationTest
	public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
		
		if (logger.isDebugEnabled()) {
			logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
		}
		
		ensureSpringRulesAreNotPresent(clazz);
		
		// **********************************************************
		// 2. 创建:TestContextManager
		// **********************************************************
		this.testContextManager = createTestContextManager(clazz);
	} // end SpringJUnit4ClassRunner

	
	// 3. 创建:TestContextManager实例
	protected TestContextManager createTestContextManager(Class<?> clazz) {
		return new TestContextManager(clazz);
	} // end 	createTestContextManager
	
}	

(5). TestContextManager

public class TestContextManager {
	
	private final TestContext testContext;
	
	
	public TestContextManager(Class<?> testClass) {
		this(
			// ******************************************************************
			// 2. 解析@BootstrapWith(SpringBootTestContextBootstrapper.class)
			// ******************************************************************
			BootstrapUtils.resolveTestContextBootstrapper(
			    // ******************************************************************
				// 1. 创建BootstrapContext
				// ******************************************************************
				BootstrapUtils.createBootstrapContext(testClass)
			)
		);
	}// end TestContextManager
	
	
	static BootstrapContext createBootstrapContext(Class<?> testClass) {
		// 1.1 创建:CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate --> DefaultCacheAwareContextLoaderDelegate
		CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = createCacheAwareContextLoaderDelegate();
		Class<? extends BootstrapContext> clazz = null;
		try {
			// DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME --> org.springframework.test.context.support.DefaultBootstrapContext
			clazz = (Class<? extends BootstrapContext>) ClassUtils.forName(DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME, BootstrapUtils.class.getClassLoader());
			Constructor<? extends BootstrapContext> constructor = clazz.getConstructor(Class.class, CacheAwareContextLoaderDelegate.class);
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("Instantiating BootstrapContext using constructor [%s]", constructor));
			}
			// 1.2 创建DefaultBootstrapContext对象,包裹着:被测试的类和DefaultCacheAwareContextLoaderDelegate
			return BeanUtils.instantiateClass(constructor, testClass, cacheAwareContextLoaderDelegate);
		}
		catch (Throwable ex) {
			throw new IllegalStateException("Could not load BootstrapContext [" + clazz + "]", ex);
		}
	}// end createBootstrapContext
	
	
	private static CacheAwareContextLoaderDelegate createCacheAwareContextLoaderDelegate() {
		Class<? extends CacheAwareContextLoaderDelegate> clazz = null;
		try {
			// DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME --> org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate
			clazz = (Class<? extends CacheAwareContextLoaderDelegate>) ClassUtils.forName(DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME, BootstrapUtils.class.getClassLoader());
			
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("Instantiating CacheAwareContextLoaderDelegate from class [%s]",
					clazz.getName()));
			}
			
			// 1.1.1 创建:DefaultCacheAwareContextLoaderDelegate的实例
			return BeanUtils.instantiateClass(clazz, CacheAwareContextLoaderDelegate.class);
		}
		catch (Throwable ex) {
			throw new IllegalStateException("Could not load CacheAwareContextLoaderDelegate [" + clazz + "]", ex);
		}
	} // end createCacheAwareContextLoaderDelegate
	
	
	static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) {
		Class<?> testClass = bootstrapContext.getTestClass();

		Class<?> clazz = null;
		try {
			// ******************************************************************
			// 3. 解析被测试类上的:@BootstrapWith(SpringBootTestContextBootstrapper.class)
			// ******************************************************************
			clazz = resolveExplicitTestContextBootstrapper(testClass);
			if (clazz == null) {
				// 4. 创建默认的Context
				clazz = resolveDefaultTestContextBootstrapper(testClass);
			}
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
						testClass.getName(), clazz.getName()));
			}
			
			// ****************************************************************************
			// 5. 初始化,实际上可以理解为:Spring的容器了
			// org.springframework.boot.test.context.SpringBootTestContextBootstrapper
			// ****************************************************************************
			TestContextBootstrapper testContextBootstrapper =
					BeanUtils.instantiateClass(clazz, TestContextBootstrapper.class);
			testContextBootstrapper.setBootstrapContext(bootstrapContext);
			return testContextBootstrapper;
		} catch (IllegalStateException ex) {
			throw ex;
		} catch (Throwable ex) {
			throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz +
					"]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.",
					ex);
		}
	} // end resolveTestContextBootstrapper
	
	
	// 判断类上是否有注解@WebAppConfiguration,如果有这个注解,则解析成:WebTestContextBootstrapper
	// 否则,解析成:DefaultTestContextBootstrapper
	private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
		ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
		// WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME --> org.springframework.test.context.web.WebAppConfiguration
		AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass, WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
		
		if (attributes != null) {
			// DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME --> org.springframework.test.context.web.WebTestContextBootstrapper
			return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
		}
		
		// DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME --> org.springframework.test.context.support.DefaultTestContextBootstrapper
		return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
	} // end resolveDefaultTestContextBootstrapper
}

(6). SpringBootTestContextBootstrapper

我比较关心Properties的处理(如何让它与Apollo/Nacos整合),没想到的是Spring不会读取配置文件(application.properties),而是要求把配置文件附加在注解上

// @SpringBootTest(classes = {HelloController.class, Conf.class}, properties = {"test.key=world"})

public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
	
	public TestContext buildTestContext() {
		TestContext context = super.buildTestContext();
		verifyConfiguration(context.getTestClass());
		WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass());
		if (webEnvironment == WebEnvironment.MOCK
				&& deduceWebApplicationType() == WebApplicationType.SERVLET) {
			context.setAttribute(ACTIVATE_SERVLET_LISTENER, true);
		}
		else if (webEnvironment != null && webEnvironment.isEmbedded()) {
			context.setAttribute(ACTIVATE_SERVLET_LISTENER, false);
		}
		return context;
	}


	protected void processPropertySourceProperties(
				MergedContextConfiguration mergedConfig,
				List<String> propertySourceProperties) {
		Class<?> testClass = mergedConfig.getTestClass();
		// 读取注解上的配置
		// @SpringBootTest(classes = {HelloController.class, Conf.class}, properties = {"test.key=world"})
		String[] properties = getProperties(testClass);
		if (!ObjectUtils.isEmpty(properties)) {
			// 把所有的配置合并在一起
			propertySourceProperties.addAll(0, Arrays.asList(properties));
		}
		
		// 判断是否为:RANDOM_PORT
		if (getWebEnvironment(testClass) == WebEnvironment.RANDOM_PORT) {
			// 定义随机端口.
			propertySourceProperties.add("server.port=0");
		}
	}
}

(7). 总结

Spring在单元测试方面还是比较用心的,只是,代码有一点点乱!