在《JUnit 源码分析》中,我们提到 @RunWith 注解具有很好的扩展性。
1. 核心原理
@RunWith 注解具有很好的扩展性,spring-test 扩展出 SpringRunner,它继承自 BlockJUnit4ClassRunner。所有的故事都是从 SpringRunner 展开,相比于 JUnit 4,SpringRunner 多出以下功能:
- 管理 Spring 上下文环境 ApplicationContext。DefaultCacheAwareContextLoaderDelegate 根据配置类缓存了对应的 ApplicationContext,如果配置信息相同则不会创建新的 ApplicationContext。
对测试类进行依赖注入。DependencyInjectionTestExecutionListener 对测试类进行依赖注入。
@RunWith(SpringRunner.class)
public class MyTest {
}
1.1 启动流程
SpringRunner 测试类的启动过程如下:
获取启动类。SpringRunner 首先获取 @BootstrapWith 注解上的启动类 TestContextBootstrapper。
- 解析配置类。 @ContextConfiguration 等注解上的配置信息,生成最终的配置类 MergedContextConfiguration。
- 初始化上下文。ContextLoader 根据配置类 MergedContextConfiguration 加载上下文环境 ApplicationContext。
- 缓存。CacheAwareContextLoaderDelegate 相同配置类 MergedContextConfiguration 会缓存对应的 ApplicationContext。
- 属性注入。通过监听器 DependencyInjectionTestExecutionListener 注入属性。
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 方法。 说明: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。
说明:到现在为止,我们已经看到 ApplicationContext。@Override
public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
prepareContext(context);
prepareContext(context, mergedConfig);
customizeBeanFactory(context.getDefaultListableBeanFactory());
loadBeanDefinitions(context, mergedConfig);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context); // 注解驱动
customizeContext(context);
customizeContext(context, mergedConfig);
context.refresh();
return context;
}
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
**总结:**到现在为止,Spring 的上下文已经做准备好了。
<a name="70A5A"></a>
### 1.2.4 TestExecutionListener
TestContextManager 管理所有的监听器,在 testClass 执行前后触发事件。当SpringRunner 创建 Test 时会调用监听器进行属性注入。调用链如下:
SpringJUnit4ClassRunner#createTest -> TestContextManager#prepareTestInstance -> DependencyInjectionTestExecutionListener#prepareTestInstance -> injectDependencies
其中 DependencyInjectionTestExecutionListener 用于在测试类启动前进行属性注入。prepareTestInstance 或 beforeTestMethod 方法都会调用 injectDependencies 进行属性注入。
```java
protected void injectDependencies(TestContext testContext) throws Exception {
Object bean = testContext.getTestInstance();
Class<?> clazz = testContext.getTestClass();
AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}
总结:可以看到,SpringRunner 继承自 JUnit4Runner,除了需要管理 Spring 上下文环境外,其它基本相同。最后总结一下,SpringRunner 的整个生命周期:
# 执行顺序
TestExecutionListener#prepareTestInstance
TestExecutionListener#beforeTestClass
TestExecutionListener#beforeTestExecution
TestExecutionListener#beforeTestMethod
TestExecutionListener#afterTestMethod
TestExecutionListener#afterTestExecution
TestExecutionListener#afterTestClass
除了装配 DependencyInjectionTestExecutionListener 之外, AbstractTestContextBootstrapper.getDefaultTestExecutionListenerClassNames 默认会装配以下监听器。
org.springframework.test.context.TestExecutionListener = \
org.springframework.test.context.web.ServletTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
org.springframework.test.context.event.EventPublishingTestExecutionListener
2. JDBC 和 事务
JDBC 和 事务都是通过 TestExecutionListener 进行扩展,所以掌握 TestExecutionListener 的生命周期非常重要。
- SqlScriptsTestExecutionListener:JDBC 处理,在方法执行前后执行 SQL 脚本或语句。
TransactionalTestExecutionListener:事务处理。
2.1 JDBC
SqlScriptsTestExecutionListener 在测试方法执行前后会读取 @Sql、@SqlGroup、@SqlConfig、@SqlMergeMode 注解上的 SQL 语句执行。
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"classpath:spring-context-jdbc.xml"})
public class SpringJdbcH2Test {
@Test
@Sql(statements = "insert into sys_user(id,name,passwd) values(2, 'binarylei2', '123456');",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(statements = "delete from sys_user where id=2;",
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void test1() {
List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
"select * from sys_user where id=2");
Assert.assertEquals(1L, mapList.size());
}
}
2.2 事务
TransactionalTestExecutionListener 默认在执行测试方法后回滚 SQL。相关的注解有 @Rollback、@Commit、@AfterTransaction、@BeforeTransaction,前两个注解表示测试方法执行完成后是否回滚(默认回滚),后两个方法表示事务前后需要执行的方法。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SpringTransactionalTests.SpringJdbcConfiguration.class)
@Transactional
public class SpringTransactionalTests {
@Test
@Sql(statements = "insert into sys_user(id,name,passwd) values(2, 'binarylei2', '123456');",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public void test2() {
List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
"select * from sys_user where id=2");
Assert.assertEquals(1L, mapList.size());
}
// 默认自动回滚SQL,所以test1不会对test2造成影响
@Test
public void test3() {
List<Map<String, Object>> mapList = jdbcTemplate.queryForList(
"select * from sys_user where id=2");
Assert.assertTrue(mapList.isEmpty());
}
}
2.3 内嵌数据库
xml 方式配置:
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:db/ddl.sql"/>
<jdbc:script location="classpath:db/dml.sql"/>
</jdbc:embedded-database>
或 JavaConfig 方式配置:
@Bean
public EmbeddedDatabase embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:db/ddl.sql")
.generateUniqueName(true)
.build();
}
3. WEB
如果测试类上标注 @WebAppConfiguration 注解,则说明是 WEB 环境,使用 WebTestContextBootstrapper 启动类,WebDelegatingSmartContextLoader 加载 WebApplicationContext。
4. 总结时刻
推荐阅读
- 《Spring Testing 5.2.7》:官方文档。
每天用心记录一点点。内容也许不重要,但习惯很重要!