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);
    }
}
相關文章
相關標籤/搜索