單元測試:大部分項目在作單元測試時因爲用到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
經過查看源碼的過程,大體明白了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); } }