在《JUnit 源码分析》中,我们提到 @RunWith 注解具有很好的扩展性。

1. 核心原理

@RunWith 注解具有很好的扩展性,spring-test 扩展出 SpringRunner,它继承自 BlockJUnit4ClassRunner。所有的故事都是从 SpringRunner 展开,相比于 JUnit 4,SpringRunner 多出以下功能:

  1. 管理 Spring 上下文环境 ApplicationContext。DefaultCacheAwareContextLoaderDelegate 根据配置类缓存了对应的 ApplicationContext,如果配置信息相同则不会创建新的 ApplicationContext。
  2. 对测试类进行依赖注入。DependencyInjectionTestExecutionListener 对测试类进行依赖注入。

    1. @RunWith(SpringRunner.class)
    2. public class MyTest {
    3. }

    1.1 启动流程

    SpringRunner 测试类的启动过程如下:

  3. 获取启动类。SpringRunner 首先获取 @BootstrapWith 注解上的启动类 TestContextBootstrapper。

  4. 解析配置类。 @ContextConfiguration 等注解上的配置信息,生成最终的配置类 MergedContextConfiguration。
  5. 初始化上下文。ContextLoader 根据配置类 MergedContextConfiguration 加载上下文环境 ApplicationContext。
  6. 缓存。CacheAwareContextLoaderDelegate 相同配置类 MergedContextConfiguration 会缓存对应的 ApplicationContext。
  7. 属性注入。通过监听器 DependencyInjectionTestExecutionListener 注入属性。 Spring Testing - 图1

    1.2 源码分析

    Spring-test 核心类如下:
  • TestContextBootstrapper:启动类。用于读取配置信息,并加载 TestContext。
  • ContextLoader:根据配置类 MergedContextConfiguration 加载 ApplicationContext。
  • CacheAwareContextLoaderDelegate:根据配置类 MergedContextConfiguration 缓存 ApplicationContext。
  • DependencyInjectionTestExecutionListener:依赖注入。
  • MergedContextConfiguration:包括 @ContextConfiguration 等注解在内的所有配置信息。
  • TestContextManager:管理 TestContext 和所有的事件 TestExecutionListener 触发。

    1.2.1 TestContextBootstrapper

    和 @RunWith 一样,Spring 也提供了 @BootstrapWith 注解来指定 TestContextBootstrapper 的实现类。如果没有标注 @BootstrapWith 注解,则非 Web 环境默认为 DefaultTestContextBootstrapper,Web 环境下则为 WebTestContextBootstrapper,具体代码见 BootstrapUtils.resolveTestContextBootstrapper 方法。 Spring Testing - 图2说明:TestContextBootstrapper 主要是解析配置类,并创建上下文环境。

  • 解析配置类:buildMergedContextConfiguration 方法将 @ContextConfiguration 注解解析为 MergedContextConfiguration 配置类。其中,几个最重根据属性解析如下:

    • ContextLoader:由 resolveContextLoader 方法解析。
    • ContextCustomizerFactory:由 getContextCustomizers 方法解析。
    • TestExecutionListener:由 getTestExecutionListeners 方法解析。
  • 创建上下文环境:buildTestContext 方法根据配置类 MergedContextConfiguration 创建 DefaultTestContext。当调用 DefaultTestContext.getApplicationConetxt 时通过 ContextLoader.loadContext 创建 Spring 上下文环境 ApplicationContext。

注意:CacheAwareContextLoaderDelegate 会将相同配置类 MergedContextConfiguration 对应的 ApplicationContext 缓存起来,不会重复调用 ContextLoader 创建。

1.2.2 ContextLoader

ContextLoader 的功能很简单,根据配置类 MergedContextConfiguration 创建 ApplicaitonContext。

  • DefaultTestContextBootstrapper:对应 DelegatingSmartContextLoader,包括 GenericXmlContextLoader 和 AnnotationConfigContextLoader。
  • WebTestContextBootstrapper:对应 WebDelegatingSmartContextLoader,包括 GenericXmlWebContextLoader 和 AnnotationConfigWebContextLoader。
    1. @Override
    2. public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
    3. GenericApplicationContext context = new GenericApplicationContext();
    4. prepareContext(context);
    5. prepareContext(context, mergedConfig);
    6. customizeBeanFactory(context.getDefaultListableBeanFactory());
    7. loadBeanDefinitions(context, mergedConfig);
    8. AnnotationConfigUtils.registerAnnotationConfigProcessors(context); // 注解驱动
    9. customizeContext(context);
    10. customizeContext(context, mergedConfig);
    11. context.refresh();
    12. return context;
    13. }
    说明:到现在为止,我们已经看到 ApplicationContext。

    1.2.3 CacheAwareContextLoaderDelegate

    现在还有一个问题,如果每个单元测试类都创建一个 Spring 上下文,那么可能会导致单元测试耗时太长。特别是 Spring Boot 默认会加载很多组件,启动就更慢了,假如单个 Spring Boot 启动需要 4s,那么 100 个测试类就需要 400s=6min,这基本上不能接受。
    那怎么解决这个问题呢?Spring 提出的方案是根据配置类 MergedContextConfiguration 缓存上下文。默认的实现类为 DefaultCacheAwareContextLoaderDelegate,其内部的缓存实现是 DefaultContextCache,这是一个 static 字段,全局唯一。LruCache 直接使用 LinkedHashMap 实现。 ```java static final ContextCache defaultContextCache = new DefaultContextCache();

public DefaultContextCache() { private final Map contextMap = Collections.synchronizedMap(new LruCache(32, 0.75f)); }

  1. **总结:**到现在为止,Spring 的上下文已经做准备好了。
  2. <a name="70A5A"></a>
  3. ### 1.2.4 TestExecutionListener
  4. TestContextManager 管理所有的监听器,在 testClass 执行前后触发事件。当SpringRunner 创建 Test 时会调用监听器进行属性注入。调用链如下:

SpringJUnit4ClassRunner#createTest -> TestContextManager#prepareTestInstance -> DependencyInjectionTestExecutionListener#prepareTestInstance -> injectDependencies

  1. 其中 DependencyInjectionTestExecutionListener 用于在测试类启动前进行属性注入。prepareTestInstance beforeTestMethod 方法都会调用 injectDependencies 进行属性注入。
  2. ```java
  3. protected void injectDependencies(TestContext testContext) throws Exception {
  4. Object bean = testContext.getTestInstance();
  5. Class<?> clazz = testContext.getTestClass();
  6. AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
  7. beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
  8. beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
  9. testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
  10. }

总结:可以看到,SpringRunner 继承自 JUnit4Runner,除了需要管理 Spring 上下文环境外,其它基本相同。最后总结一下,SpringRunner 的整个生命周期:

  1. # 执行顺序
  2. TestExecutionListener#prepareTestInstance
  3. TestExecutionListener#beforeTestClass
  4. TestExecutionListener#beforeTestExecution
  5. TestExecutionListener#beforeTestMethod
  6. TestExecutionListener#afterTestMethod
  7. TestExecutionListener#afterTestExecution
  8. TestExecutionListener#afterTestClass

除了装配 DependencyInjectionTestExecutionListener 之外, AbstractTestContextBootstrapper.getDefaultTestExecutionListenerClassNames 默认会装配以下监听器。

  1. org.springframework.test.context.TestExecutionListener = \
  2. org.springframework.test.context.web.ServletTestExecutionListener,\
  3. org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
  4. org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
  5. org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
  6. org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
  7. org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
  8. org.springframework.test.context.event.EventPublishingTestExecutionListener

2. JDBC 和 事务

JDBC 和 事务都是通过 TestExecutionListener 进行扩展,所以掌握 TestExecutionListener 的生命周期非常重要。

  • SqlScriptsTestExecutionListener:JDBC 处理,在方法执行前后执行 SQL 脚本或语句。
  • TransactionalTestExecutionListener:事务处理。

    2.1 JDBC

    SqlScriptsTestExecutionListener 在测试方法执行前后会读取 @Sql、@SqlGroup、@SqlConfig、@SqlMergeMode 注解上的 SQL 语句执行。

    1. @RunWith(SpringRunner.class)
    2. @ContextConfiguration(locations = {"classpath:spring-context-jdbc.xml"})
    3. public class SpringJdbcH2Test {
    4. @Test
    5. @Sql(statements = "insert into sys_user(id,name,passwd) values(2, 'binarylei2', '123456');",
    6. executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    7. @Sql(statements = "delete from sys_user where id=2;",
    8. executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    9. public void test1() {
    10. List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
    11. "select * from sys_user where id=2");
    12. Assert.assertEquals(1L, mapList.size());
    13. }
    14. }

    2.2 事务

    TransactionalTestExecutionListener 默认在执行测试方法后回滚 SQL。相关的注解有 @Rollback、@Commit、@AfterTransaction、@BeforeTransaction,前两个注解表示测试方法执行完成后是否回滚(默认回滚),后两个方法表示事务前后需要执行的方法。

    1. @RunWith(SpringRunner.class)
    2. @ContextConfiguration(classes = SpringTransactionalTests.SpringJdbcConfiguration.class)
    3. @Transactional
    4. public class SpringTransactionalTests {
    5. @Test
    6. @Sql(statements = "insert into sys_user(id,name,passwd) values(2, 'binarylei2', '123456');",
    7. executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    8. public void test2() {
    9. List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
    10. "select * from sys_user where id=2");
    11. Assert.assertEquals(1L, mapList.size());
    12. }
    13. // 默认自动回滚SQL,所以test1不会对test2造成影响
    14. @Test
    15. public void test3() {
    16. List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
    17. "select * from sys_user where id=2");
    18. Assert.assertTrue(mapList.isEmpty());
    19. }
    20. }

    2.3 内嵌数据库

    xml 方式配置:

    1. <jdbc:embedded-database id="dataSource" type="H2">
    2. <jdbc:script location="classpath:db/ddl.sql"/>
    3. <jdbc:script location="classpath:db/dml.sql"/>
    4. </jdbc:embedded-database>

    或 JavaConfig 方式配置:

    1. @Bean
    2. public EmbeddedDatabase embeddedDatabase() {
    3. return new EmbeddedDatabaseBuilder()
    4. .setType(EmbeddedDatabaseType.H2)
    5. .addScript("classpath:db/ddl.sql")
    6. .generateUniqueName(true)
    7. .build();
    8. }

    3. WEB

    如果测试类上标注 @WebAppConfiguration 注解,则说明是 WEB 环境,使用 WebTestContextBootstrapper 启动类,WebDelegatingSmartContextLoader 加载 WebApplicationContext。

4. 总结时刻

推荐阅读

  1. Spring Testing 5.2.7》:官方文档。

每天用心记录一点点。内容也许不重要,但习惯很重要!