本文使用的是JUnit4.12 Spring test5.0.7.RELEASE源碼java
前不久遇到一個問題,一個Spring Boot項目的普通單元測試可以正常使用Spring test的特性,例如依賴注入、事務管理等,一旦使用JUnit4提供的@RunWith(Parameterized.class)參數化測試後,Spring test的特性便再也不適用。查遍網上的中文資料,都沒有給出完美的解決方案,更多的是簡單調用TestContextManager.prepareTestInstance()來實現Spring的初始化,沒有解決事務管理等Spring test特性不可用的問題。基於緣由,我決定好好研究一下JUnit4與Spring test的源碼,理解它們的實現原理、拓展機制,找到方法完全解決這類問題。node
以上問題能夠拆分紅四個子問題,分別是:spring
(1) 關鍵類的介紹
複製代碼
Runner 描述一個測試案例整體上該如何執行,其核心是run(RunNotifier)方法,其中RunNotifier用於發佈通知
ParentRunner 繼承自Runner,源碼中的註釋是這樣的設計模式
Provides most of the functionality specific to a Runner that implements a "parent node" in the test tree, with children defined by objects of some data type T. (For BlockJUnit4ClassRunner, T is Method . For Suite, T is Class.)數組
大意就是把測試案例構形成相似Tree的結構,child是泛型T,對於BlockJUnit4ClassRunner來講T是Method, 而Suite的T是Class
BlockJUnit4ClassRunner JUnit4的默認Runner,繼承自ParentRunner
Statement 描述一個JUnit4單元測試具體要作的事情,是JUnit4拓展的核心,它只有一個方法evaluate()
RunnerBuilder 描述如何構建一組Runner
JUnitCore JUnit4最開始啓動的地方框架
(2) 一個單元測試的運行過程
複製代碼
一般在IDE手動運行單元測試查看案例缺陷率,代碼覆蓋率都是由Eclipse/Intellij IDEA等IDE做爲主入口,而後調起run(Request)獲取該測試案例的Runneride
public Result run(Request request) {
return run(request.getRunner());
}
複製代碼
隨後默認會以IgnoredBuilder, AnnotatedBuilder, SuiteMethodBuilder, Junit3Builder, Junit4Builder的前後順序尋找適合的RunnerBuilderspring-boot
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
List<RunnerBuilder> builders = Arrays.asList(
ignoredBuilder(),
annotatedBuilder(),
suiteMethodBuilder(),
junit3Builder(),
junit4Builder());
for (RunnerBuilder each : builders) {
Runner runner = each.safeRunnerForClass(testClass);
if (runner != null) {
return runner;
}
}
return null;
}
複製代碼
IgnoredBuilder的優先級最高,尋找測試類是否有@Ignore註解,建立IgnoredClassRunner,該測試案例會被忽略
AnnotatedBuilder的優先級次高,咱們大多數測試案例都屬於這種狀況.它尋找測試類是否有@RunWith註解,反射調用(Class)構造方法,若是找不到則調用(Class,RunnerBuilder)構造方法單元測試
@Override
public Runner runnerForClass(Class<?> testClass) throws Exception {
for (Class<?> currentTestClass = testClass; currentTestClass != null;
currentTestClass = getEnclosingClassForNonStaticMemberClass(currentTestClass)) {
RunWith annotation = currentTestClass.getAnnotation(RunWith.class);
if (annotation != null) {
return buildRunner(annotation.value(), testClass);
}
}
return null;
}
public Runner buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass) throws Exception {
try {
return runnerClass.getConstructor(Class.class).newInstance(testClass);
} catch (NoSuchMethodException e) {
try {
return runnerClass.getConstructor(Class.class,
RunnerBuilder.class).newInstance(testClass, suiteBuilder);
} catch (NoSuchMethodException e2) {
String simpleName = runnerClass.getSimpleName();
throw new InitializationError(String.format(
CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName));
}
}
}
複製代碼
再到SuiteMethodBuilder,尋找命名爲suite的method
Junit3Builder會判斷測試類是否TestCase的子類,這是爲了兼容JUnit3
若是以上條件都不符合,就會使用Junit4Builder,默認使用BlockJUnit4ClassRunner,所以若是咱們的測試案例沒有額外的註解,都是使用BlockJUnit4ClassRunner運行測試
肯定了Runner後,JUnitCore就執行run(Runner),調用Runner.run(Notifier)
public Result run(Runner runner) {
Result result = new Result();
RunListener listener = result.createListener();
notifier.addFirstListener(listener);
try {
notifier.fireTestRunStarted(runner.getDescription());
runner.run(notifier);
notifier.fireTestRunFinished(result);
} finally {
removeListener(listener);
}
return result;
}
複製代碼
不一樣的Runner實現方式不一樣,下面以默認的BlockJUnit4ClassRunner爲例分析
BlockJUnit4ClassRunner沒有重寫run()方法,所以調用了父類ParentRunner.run()。 方法邏輯很簡單, classBlock()構建一個statement,而後執行statement的evaluate()
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
try {
Statement statement = classBlock(notifier);
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
testNotifier.addFailure(e);
}
}
複製代碼
咱們看一下classBlock
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
複製代碼
childrenInvoker()構造了一個statement,執行runChildren()方法,由於是ParentRunner,因此對它來講主要是runChildren(),至於withBeforeClasses(),withAfterClasses()的做用,咱們稍後再分析
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
複製代碼
其邏輯也十分簡單,根據getChildren()獲取Children列表,而後逐個調用runChild()。getChildren()和runChild()在ParentRunner中都沒有實現,咱們來看一會兒類BlockJUnit4ClassRunner的實現
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
複製代碼
getChildren()尋找測試類中被@Test註解的方法
runChild()則是對每一個child(在BlockJUnit4ClassRunner中就是method)調用methodBlock()封裝,而後調用statement.evaluate(),執行整個測試方法
BlockJUnit4ClassRunner的運行過程以下所示
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
Statement statement = methodInvoker(method, test);
statement = possiblyExpectingExceptions(method, test, statement);
statement = withPotentialTimeout(method, test, statement);
statement = withBefores(method, test, statement);
statement = withAfters(method, test, statement);
statement = withRules(method, test, statement);
return statement;
}
複製代碼
咱們能夠看到classBlock()和methodBlock()不停對statement進行包裝,上一個方法返回的statement做爲下一個方法的參數.這種設計模式與責任鏈,Struts2的攔截器十分類似,每一次包裝都會把參數statement做爲next,而後調用自身的邏輯 假設有一個測試類TestClass,他有兩個測試方法,分別是testMethodA()和testMethodB(),那麼這個測試類的運行圖以下所示
這便徹底與咱們使用JUnit4的註解對應上了,至此,咱們已經瞭解JUnit4內部的工做原理
那麼Spring做爲Java開發的核心框架,他是如何把自身的test特性拓展在JUnit4上?
關鍵在於TestContextManager和SpringJUnit4ClassRunner(或者叫作SpringRunner),後者繼承BlockJUnit4ClassRunner並重寫了部分方法
@Override
protected Statement methodBlock(FrameworkMethod frameworkMethod) {
Object testInstance;
try {
testInstance = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
}
catch (Throwable ex) {
return new Fail(ex);
}
Statement statement = methodInvoker(frameworkMethod, testInstance);
statement = withBeforeTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = withAfterTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
statement = withBefores(frameworkMethod, testInstance, statement);
statement = withAfters(frameworkMethod, testInstance, statement);
statement = withRulesReflectively(frameworkMethod, testInstance, statement);
statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
return statement;
}
複製代碼
咱們能夠看到他與BlockJUnit4ClassRunner.methodBlock()大體相同,區別在於他額外添加了BeforeTestExecution, AfterTestExecution和PotentialRepeat,前二者是Spring test自身添加的包裝點,不在JUnit4默認提供的包裝點中,後者是對@Repeat註解的支持
除此以外,Spring test還提供了TestExecutionListener監聽器,7個接口方法對應測試案例的7個包裝點
還有一點必需要提到的是,在SpringJUnit4ClassRunner被JUnitCore初始化的時候,會建立Spring的TestContextManager,他會找到classpath下META-INF/spring.factories中定義好的TestExecutionListener,Spring Boot項目一般會找到12個,分別在spring-test,spring-boot-test和spring-boot-test-autoconfigure三個jar包下,所以若是要根據項目需求自定義TestExecutionListener,只須要按照上述方式設計即可注入到測試案例的生命週期中
@Override
public void beforeTestMethod(final TestContext testContext) throws Exception {
Method testMethod = testContext.getTestMethod();
Class<?> testClass = testContext.getTestClass();
// 省略部分代碼
tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
Assert.state(tm != null,
() -> "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
}
// 開啓事務
if (tm != null) {
txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
runBeforeTransactionMethods(testContext);
txContext.startTransaction();
TransactionContextHolder.setCurrentTransactionContext(txContext);
}
}
複製代碼
他實現了beforeTestMethod()和afterTestMethod(),在這兩個方法中開啓/提交/回滾事務
綜上, SpringJUnit4ClassRunner的一個statement有這麼多運行過程(我也很佩服我本身居然畫了出來)
在JUnit4裏,@RunWith裏常常搭配BlockJUnit4ClassRunner或Parameterized參數, BlockJUnit4ClassRunner在上文已經詳細介紹了,下面介紹一下Parameterized
Parameterized繼承自Suite,Suite用於多個測試類合在一塊兒跑,而Parameterized是一個測試類對多組參數執行屢次,與Suite本質上很相似,也就理解爲什麼Parameterized會繼承Suite了
Suite繼承自ParentRunner,注意不一樣於BlockJUnit4ClassRunner繼承自ParentRunner,說明Suite的children是Runner,比BlockJUnit4ClassRunner更高一級.所以Suite的getChildren()就是返回runners ,而runChild()就是對runner調用run()
@Override
protected List<Runner> getChildren() {
return runners;
}
@Override
protected void runChild(Runner runner, final RunNotifier notifier) {
runner.run(notifier);
}
複製代碼
Parameterized與Suite的差別在於,Suite的Children是測試類集合對應的Runner集合.
Parameterized根據有多少組參數化數組,就構建多少組BlockJUnit4ClassRunnerWithParameters,而後將每一組參數化數組注入到每個Runner中,以後就能夠像Suite同樣runChild()
結合上文描述的Spring test 和Parameterized可知,參數化測試默認使用的Runner是BlockJUnit4ClassRunnerWithParameters,它繼承了BlockJUnit4ClassRunner實現了注入參數的功能,沒有Spring test的特性
在Parameterized的源碼註釋中有一段話給了咱們提示
By default the Parameterized runner creates a slightly modified BlockJUnit4ClassRunner for each set of parameters. You can build an own Parameterized runner that creates another runner for each set of parameters. Therefore you have to build a ParametersRunnerFactory that creates a runner for each TestWithParameters. (TestWithParameters are bundling the parameters and the test name.) The factory must have a public zero-arg constructor. Use the Parameterized.UseParametersRunnerFactory to tell the Parameterized runner that it should use your factory.
大意就是若是想自定義Parameterized的Runner,請從新實現ParametersRunnerFactory並構建一個能夠注入參數的Runner,而後用@ UseParametersRunnerFactory註解來指定自定義的工廠.
因此咱們只須要設計一個類SpringJUnit4ClassRunnerWithParametersFactory繼承SpringJUnit4ClassRunner,確保支持Spring test的特性,而後從新加入注入參數的功能就行了,這部分功能能夠參考BlockJUnit4ClassRunnerWithParameters來實現.
在實現的過程當中咱們發現SpringJUnit4ClassRunner和BlockJUnit4ClassRunnerWithParameters都重寫了createTest()方法,那咱們只需把兩個方法融合在一塊兒就行了 最後效果以下
@Override
public Object createTest() throws Exception {
Object testInstance;
if (fieldsAreAnnotated()) {
testInstance = createTestUsingFieldInjection();
} else {
testInstance = createTestUsingConstructorInjection();
}
getTestContextManager().prepareTestInstance(testInstance);
return testInstance;
}
複製代碼
固然這不是惟一的解決方法,Spring test提供了另一種更通用的解決方法,就是在本來BlockJUnit4ClassRunner的Rules和ClassRules處添加上Spring test的功能
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
複製代碼
但這種方法有個弊端,因爲TestExecutionListener接口定義了3套注入點,JUnit4只提供了2個注入的地方,對於before/afterTestExecution是沒法注入的,要千萬注意!
下面也是官方源碼中提醒咱們注意(吐槽JUnit4的缺點)
WARNING: Due to the shortcomings of JUnit rules, the SpringMethodRule does not support the beforeTestExecution() and afterTestExecution() callbacks of the TestExecutionListener API.
所幸的是,Spring test提供的12個TestExecutionListener都沒有使用before/afterTestExecution,因此那12個TestExecutionListener在這種方式下仍能正常運行
JUnit4和Spring test的源碼仍是比較容易理解的,不得不說,調試+堆棧+註釋真是理解源碼的三大法寶。固然了JUnit4和Spring test的內容不只僅是這些,本文只是將關鍵設計抽出來分析,技術就是一個越挖越深的巨坑啊
在閱讀源碼的時候還發現了一些有趣的地方, SpringJUnit4ClassRunner在加載的時候,會找出JUnit4的withRules方法,用反射改爲public,彷彿在吐槽JUnit4不把這個方法開放,但Spring偏要擴展這個方法
static {
Assert.state(ClassUtils.isPresent("org.junit.internal.Throwables", SpringJUnit4ClassRunner.class.getClassLoader()),
"SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
Method method = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules",
FrameworkMethod.class, Object.class, Statement.class);
Assert.state(method != null, "SpringJUnit4ClassRunner requires JUnit 4.12 or higher");
ReflectionUtils.makeAccessible(method);
withRulesMethod = method;
}
複製代碼
至於爲何會分析JUnit4,由於Spring-boot-starter-test默認就引入了JUnit4,我也不知道爲何不引入JUnit5.若是有小夥伴知道緣由麻煩私信留言告訴我.基於JUnit5的新特性,後續可能會使用它,若是到時候有新的發現會再寫一篇文章
感謝小夥伴耐心看完了整篇文章,若是以爲對你有一點幫助,不妨關注一下