单元测试:大部分项目在作单元测试时因为用到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); } }