SpringJUnit4ClassRunner 类扩展

原因

单元测试:大部分项目在作单元测试时因为用到spring,因此会引入spring-test 结合junit4 来写单元测试,并且会用到 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:consumer.xml") 两个个注解来指定 junit 的 runner 为 SpringJUnit4ClassRunner 从而实现 spring 容器的启动。java

/**
 * Unit test for simple App.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:consumer.xml")
public class AppTest {

    @Autowired
    BeanFactory beanFactory;

    @Test
    public void runTest() {
        Assert.assertNotNull("spring start fail", beanFactory);
    }
}

问题:可是若是项目中的单元测试愈来愈多,每一个Test测试类上都要加上这两个文件,并且万一新增configuration配置项将是一笔不小的改动。spring

方案

尝试实现本身的 Junit Runner, 这样在Test类上只需指定@RunWith 为自定的 Runner就能实现spring contxt 的加载来启动整个spring容器ide

过程

查看 SpringJUnit4ClassRunner 内部实现单元测试

/**
	 * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a
	 * {@link TestContextManager} to provide Spring testing functionality to
	 * standard JUnit tests.
	 * @param clazz the test class to be run
	 * @see #createTestContextManager(Class)
	 */
	public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
		if (logger.isDebugEnabled()) {
			logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "].");
		}
		this.testContextManager = createTestContextManager(clazz);
	}
	
	
	/**
	 * Creates a new {@link TestContextManager} for the supplied test class and
	 * the configured <em>default {@code ContextLoader} class name</em>.
	 * Can be overridden by subclasses.
	 * @param clazz the test class to be managed
	 * @see #getDefaultContextLoaderClassName(Class)
	 */
	protected TestContextManager createTestContextManager(Class<?> clazz) {
		return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz));
	}

以上源码得知咱们能够覆盖createTestContextManager来实现本身的contextLoader 来初始化 TestContextManager, 咱们进一步查看TestContextManager的初始化过程测试

// TestContextManager 的初始化过程,涉及到 TestContext 和 注册监听
public TestContextManager(Class<?> testClass, String defaultContextLoaderClassName) {
		this.testContext = new TestContext(testClass, contextCache, defaultContextLoaderClassName);
		registerTestExecutionListeners(retrieveTestExecutionListeners(testClass));
	}
	
// TestContext 的初始化过程
TestContext(Class<?> testClass, ContextCache contextCache, String defaultContextLoaderClassName) {
		Assert.notNull(testClass, "Test class must not be null");
		Assert.notNull(contextCache, "ContextCache must not be null");

		this.testClass = testClass;
		this.contextCache = contextCache;
		this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache);

		MergedContextConfiguration mergedContextConfiguration;

		if (testClass.isAnnotationPresent(ContextConfiguration.class)
				|| testClass.isAnnotationPresent(ContextHierarchy.class)) {
			mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
				defaultContextLoaderClassName, cacheAwareContextLoaderDelegate);
		}
		else {
			if (logger.isInfoEnabled()) {
				logger.info(String.format(
					"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]",
					testClass.getName()));
			}
			mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null);
		}

		this.mergedContextConfiguration = mergedContextConfiguration;
	}

经过上述的 TestContext 出事过程能够得知,在经过单元测试类上的 @ContextConfiguration 或 @ContextHierarchy 来加载spring configuration来完成spring testContext的初始化。ui

自定义 Runner

经过查看源码的过程,大体明白了SpringJUnit4ClassRunner的配置加载过程,接下来经过自定有Runner来实现spring configuration的配置来实现spring 测试容器的自启, 代码以下:this

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.runners.model.InitializationError;
import org.springframework.beans.BeanUtils;
import org.springframework.test.context.*;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DelegatingSmartContextLoader;

/**
 * 自定义 junit runner
 */
public class DemoRunner extends SpringJUnit4ClassRunner {

    /**
     * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a
     * {@link TestContextManager} to provide Spring testing functionality to
     * standard JUnit tests.
     *
     * @param clazz the test class to be run
     *
     * @see #createTestContextManager(Class)
     */
    public DemoRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }

    /**
     * 建立 spring context
     * @param clazz
     * @return
     */
    @Override
    protected TestContextManager createTestContextManager(Class<?> clazz) {
        TestContextManager testContextManager = super.createTestContextManager(clazz);
        Field testContextField;
        try {
            // 提取 testContext
            testContextField = testContextManager.getClass().getDeclaredField("testContext");
            testContextField.setAccessible(true);
            TestContext testContext = (TestContext)testContextField.get(testContextManager);
            Field configurationField = TestContext.class.getDeclaredField("mergedContextConfiguration");
            configurationField.setAccessible(true);
            Constructor<?>[] constructors = Class.forName("org.springframework.test.context.ContextCache").getDeclaredConstructors();
            constructors[0].setAccessible(true);
            Object contextCache = constructors[0].newInstance();
            Constructor<?>[] cacheAwareContextConstructors = CacheAwareContextLoaderDelegate.class.getDeclaredConstructors();
            cacheAwareContextConstructors[0].setAccessible(true);
            CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = (CacheAwareContextLoaderDelegate) cacheAwareContextConstructors[0].newInstance(contextCache);
            // 手动初始化MergedContextConfiguration 设置到 testContext
            configurationField.set(testContext,
                    new MergedContextConfiguration(clazz, new String[] {"classpath:consumer.xml"},null, null, null,
                    BeanUtils.instantiateClass(DelegatingSmartContextLoader.class, ContextLoader.class), cacheAwareContextLoaderDelegate, null));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return testContextManager;
    }
}

因为SpringJUnit4ClassRunner中不少变量、方法 都是已内部类、私有属性,因此直接经过反射方式在初始化TestContext时插入本身配置的MergedContextConfiguration来完成spring configuration。spa

测试

经过以上的代码咱们实现了本身的Junit Runner,名为DemoRunner; 如今咱们能够经过@RunWith(DemoRunner.class) 来完成spring+junit4的启动配置:debug

/**
 * Unit test for simple App.
 */
@RunWith(DemoRunner.class)
public class AppTest {

    @Autowired
    BeanFactory beanFactory;

    @Test
    public void runTest() {
        Assert.assertNotNull("spring start fail", beanFactory);
    }
}
相关文章
相关标签/搜索