(點選上方公眾號,可快速關註)
來源:saymagic ,
blog.saymagic.cn/2016/09/30/understand-Junit.html#post__title
JUnit是由 Erich Gamma 和 Kent Beck 編寫的一個回歸測試框架,以Eclipse、IDEA等為代表的Java開發環境都對JUnit提供了非常友善的支援。提到Erich Gamma,他就是大名鼎鼎的《設計樣式:可復用面向物件軟體的基礎》一書的作者之一。因此,JUnit當中的設計樣式的運用相當得當,所以,JUnit的原始碼可謂相當優良的一本武林秘籍,非常值得一看。 本文基於JUnit4.12,將從JUnit的執行流程,Match驗證,兩個方面,來對JUnit的原始碼進行整體的分析。
執行流程
JUnit的啟動方式有很多,比如在Android Studio中我們可以直接點選某個被@Test註解的函式來執行:
此時,啟動的是JUniteStarter,該類是intellij為我們提供的。感興趣可以檢視其原始碼:
https://github.com/JetBrains/intellij-community/blob/master/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnitStarter.java
如果我們使用gradle, 可以執行gradle test執行測試,實際上是在一個執行緒中執行SuiteTestClassProcessor的processTestClass方法來進行啟動。其原始碼可以檢視
https://github.com/gradle/gradle/blob/master/subprojects/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/SuiteTestClassProcessor.java
如上兩種都是第三方工具為我們提供的便捷方式,實際上JUnit也提供了一個名為JUnitCore的類來供我們方便的執行測試用例。
儘管啟動JUnit的方式有很多,但這都是開啟與JUnit對話的一些方式,最終執行的還是JUnit當中的起到核心作用的一些類,為了讓大家對這些核心boss有一個初步瞭解,我畫了一個類圖:
上圖中僅是JUnit中的幾個核心的類,也是本分主要分析的物件。這裡先給出一些物件的職責,可以有個大體的瞭解,後面會透過程式碼就會更清楚每個物件是如何完成這些職責的:
-
在類圖的中央,有個叫做ParentRunne的物件很引人註目,它繼承自Runner.
-
Runner則表示著JUnit對整個測試的抽象
-
Runner實現了Describable介面,Describable介面中唯一的函式getDescription()傳回了Description物件,記錄著測試的資訊。
-
Statement 是一個抽象類,其 evaluate()函式代表著在測試中將被執行的方法。
-
ParentRunner 共有兩個子類,BlockJUnit4ClassRunner 用來執行單個測試類,Suite用來一起執行多個測試類
-
RunnerBuilder 是生產Runner的策略,如使用@RunWith(Suite.class)標註的類需要使用Suite, 被@Ignore標註的類需要使用IgnoreClassRunner。
-
TestClass是對被測試的類的封裝
綜上,我們先從ParentRunner看起,其建構式如下:
protected ParentRunner(Class > testClass) throws InitializationError {
this.testClass = createTestClass(testClass);
validate();
}
this.testClass即前文所說的TestClass,我們進入createTestClass方法來檢視其如何將class物件轉換為TestClass。
protected TestClass createTestClass(Class > testClass) {
return new TestClass(testClass);
}
並沒什麼東西,具體的邏輯都寫在TestClass的內部:
public TestClass(Class > clazz) {
this.clazz = clazz;
if (clazz != null && clazz.getConstructors().length > 1) {
throw new IllegalArgumentException(
“Test class can only have one constructor”);
}
Map
, List > methodsForAnnotations = new LinkedHashMap
, List >(); Map
, List > fieldsForAnnotations = new LinkedHashMap
, List >(); scanAnnotatedMembers(methodsForAnnotations, fieldsForAnnotations);
this.methodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
this.fieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
}
可以看到,整個建構式大致都在做一些驗證和初始化的工作,需要引起我們註意的應該是scanAnnotatedMembers方法:
protected void scanAnnotatedMembers(Map
, List > methodsForAnnotations, Map , List > fieldsForAnnotations) { for (Class > eachClass : getSuperClasses(clazz)) {
for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {
addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);
}
// ensuring fields are sorted to make sure that entries are inserted
// and read from fieldForAnnotations in a deterministic order
for (Field eachField : getSortedDeclaredFields(eachClass)) {
addToAnnotationLists(new FrameworkField(eachField), fieldsForAnnotations);
}
}
}
整個函式的作用就是掃描class中方法和變數上的註解,並將其根據註解的型別進行分類,快取在methodsForAnnotations與fieldsForAnnotations當中。需要註意的是,JUnit對方法和變數分別封裝為FrameworkMethod與FrameworkField,它們都繼承自FrameworkMember,這樣就為方法和變數進行了統一抽象。
看完了ParentRunner的建構式,我們來看ParentRunner繼承自Runner的run方法是如何工作的:
@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函式將notifier轉換為Statement:
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
繼續追進childrenInvoker之前,允許我現在這裡先存個檔,記為A,一會我們會回到classBlock這裡
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}
childrenInvoker傳回的是一個Statement,看它的evaluate方法,其呼叫的是runChildren方法,這也是ParentRunner中非常重要的一個函式:
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();
}
}
這個函式就體現了抽象的重要性,註意泛型T,它在ParentRunner的每個實現類中各不相同,在BlockJUnit4ClassRunner中T表示FrameworkMethod,具體到這個函式來講getFilteredChildren拿到的是被@Test註解標註的FrameworkMethod,而在Suite中,T為Runner,而ParentRunner.this.runChild(each, notifier);這句的中的runChild(each, notifier)方法依舊是個抽象方法,我們先看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);
}
}
isIgnored方法判斷了method方法是否被@Ignore註解標識,如果是的話則直接通知notifier觸發ignored事件,否則,執行runLeaf方法, runLeaf的第一個引數是Statement,所以,BlockJUnit4ClassRunner透過methodBlock方法將method轉換為Statement:
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;
}
前面的幾行程式碼是在生成test 物件,而test物件的型別則是我們待測試的class,接下來追進methodInvoker方法:
protected Statement methodInvoker(FrameworkMethod method, Object test) {
return new InvokeMethod(method, test);
}
可見,我們生成的Statement實體為InvokeMethod,我們看下其evaluate方法:
testMethod.invokeExplosively(target);
invokeExplosively函式做的事情就是對target物件呼叫testMethod方法。而前面我們說過,這個testMethod在BlockJUnit4ClassRunner中就是被@Test所標註的方法,此時,我們終於找到了@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);
我們可以看到,statement不斷的在變形,而透過withBefores,withRules這些函式的名字我們可以很容易猜到,這裡就是在處理@Before,@Rule等註解的地方,我們以withBefores為例:
protected Statement withBefores(FrameworkMethod method, Object target,
Statement statement) {
List
befores = getTestClass().getAnnotatedMethods( Before.class);
return befores.isEmpty() ? statement : new RunBefores(statement,
befores, target);
}
這個函式裡首先拿到了所有被@Before標註的方法,將其封裝為RunBefores,我們看下其建構式和
public RunBefores(Statement next, List
befores, Object target) { this.next = next;
this.befores = befores;
this.target = target;
}
public void evaluate() throws Throwable {
for (FrameworkMethod before : befores) {
before.invokeExplosively(target);
}
next.evaluate();
}
很是明瞭,evaluate執行時,首先將before方法全部invoke來執行,然後才呼叫原始statement的evaluate方法。其餘幾個函式與此類似,感興趣可以繼續檢視。
如此,我們就明白了runLeaf方法的第一個引數Statement的由來,接下來就看下這個runLeaf方法做了什麼,runLeaf在ParentRunner中有預設的實現:
protected final void runLeaf(Statement statement, Description description,
RunNotifier notifier) {
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
eachNotifier.fireTestStarted();
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
非常簡單,直接執行了statement的evaluate方法,需要註意的是這裡的statement實體不一定是什麼了,有可能是RunBefores,也有可能是RunAfters,這就和被測試類中的註解有關了。
講到這裡,還記得前面我們說過的存檔A嗎?我們回到存檔A:
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
剛剛存檔後所發生的一起,其實就是在執行Statement statement = childrenInvoker(notifier)這個程式碼。換句話說,childrenInvoker的作用就是將所有需要執行的測試用例用一個Statement封裝起來。進而點燃這個Statement,就會觸發所有的測試用例。但同樣需要註意到被if陳述句包圍的程式碼,我們又看到了熟悉的陳述句,Statement還在被不斷的轉換,但此時是在類的層面,withBeforeClasses函式操作的就是@BeforeClass註解了:
protected Statement withBeforeClasses(Statement statement) {
List
befores = testClass .getAnnotatedMethods(BeforeClass.class);
return befores.isEmpty() ? statement :
new RunBefores(statement, befores, null);
}
需要註意的是這回RunBefores的第三個引數為null,說明被@BeforeClass註解的方法只能是static的。
如上,我們分析了BlockJUnit4ClassRunner的執行流程,也就是說當測試類為一個的時候JUnit是如何工作的。前文也提到過,ParentRunner還有一個子類Suite,表示需要執行一組測試,BlockJUnit4ClassRunner的一個執行單元為FrameworkMethod,而Suite的一個執行單元為Runner,我們看其runChild方法:
protected void runChild(Runner runner, final RunNotifier notifier) {
runner.run(notifier);
}
很是明瞭,直接滴啊用runner的run方法。這樣,如果這個runner的實體仍然是Suite,則會繼續向裡執行,如果這個runner為BlockJUnit4ClassRunner,這執行我們前面分析的邏輯。這裡有個問題是,那這個runner是如何生成的呢?這就要看Suite的建構式:
protected Suite(Class > klass, Class >[] suiteClasses) throws InitializationError {
this(new AllDefaultPossibilitiesBuilder(true), klass, suiteClasses);
}
AllDefaultPossibilitiesBuilder的職責就是為每個類生找到對應的Runner,感興趣可以檢視其runnerForClass方法,比較容易理解,這裡就不再贅述。
Matcher驗證
上面我們分析了用@Test標註的函式是如何被JUnit執行的,但單單有@Test標註是肯定不夠的,既然是測試,我們肯定需要一定的手段來驗證程式的的執行是符合預期的。JUnit提供了Matcher機制,可以滿足我們大部分的需求。Matcher相關類主要在org.hamcrest包下,先來看下類圖:
上圖僅僅列出了org.hamcrest包下的一部分類,這些類一起組合起來形成了JUnit強大的驗證機制。
驗證的基本寫法是:
MatcherAssert.assertThat(“saymagic”, CoreMatchers.containsString(“magic”));
首先我們需要呼叫的是MatcherAssert的assertThat方法,這個方法最終輾轉為:
public static
void assertThat(String reason, T actual, Matcher super T> matcher) { if (!matcher.matches(actual)) {
Description description = new StringDescription();
description.appendText(reason)
.appendText(“\nExpected: “)
.appendDescriptionOf(matcher)
.appendText(“\n but: “);
matcher.describeMismatch(actual, description);
throw new AssertionError(description.toString());
}
}
這個函式目的很是明確,直接判斷matcher是否匹配,不匹配則封裝描述資訊,然後丟擲異常。所以我們來關註matcher的matchs方法都做了些什麼,CoreMatchers.containsString(“magic”)傳回的就是一個matcher, CoreMatchers相當於一個靜態工廠,提供了大量的靜態方法來傳回各種Matcher:
我們就已剛剛的containsString為例,檢視其內部程式碼:
public static org.hamcrest.Matcher
containsString(java.lang.String substring) { return org.hamcrest.core.StringContains.containsString(substring);
}
可見其呼叫了StringContains的一個靜態方法,繼續追:
@Factory
public static Matcher
containsString(String substring) { return new StringContains(substring);
}
這裡很簡單,直接new了一個StringContains實體,StringContains的繼承關係如下:
首先BaseMatcher實現了Matcher介面,TypeSafeMatcher是BaseMatcher的一個抽象實現,它的matches方法如下:
public final boolean matches(Object item) {
return item != null
&& expectedType.isInstance(item)
&& matchesSafely((T) item);
}
可見它在驗證前作了判空與型別的校驗,所以子類就可以實現matchesSafely方法,就無需在此方法中進行判空與型別的驗證了。
SubstringMatchers是TypeSafeMatcher的一種實現,它是對字串類驗證的一種抽象,它的matchesSafely方法如下:
@Override
public boolean matchesSafely(String item) {
return evalSubstringOf(item);
}
子類需要實現evalSubstringOf方法。如此,我們就可以看下StringContains的這個方法了:
@Override
protected boolean evalSubstringOf(String s) {
return s.indexOf(substring) >= 0;
}
出奇的簡單,並沒有什麼好解釋的。這個如果傳回了false,說明驗證不透過,前面的assertThat方法就會丟擲異常。這樣,JUnit的一個測試就不會透過。
assert翻譯過來為斷言,也就是說,它是用來驗證是非的,但我們也清楚,並非所有的事情都分是非,測試也如此,比如我們要測試登入模組,當點選login按鈕的時候,可能驗證透過後就跳轉了頁面,並沒有任何傳回值,這個時候我們往往會驗證某個事情發生了,比如login後執行了跳轉方法,這樣就表示測試是透過的。這就是Mock框架來做的是。感興趣的可以檢視我的上一篇文章Mockito原始碼解析。
https://blog.saymagic.tech/2016/09/17/understand-mockito.html
總結
讀懂JUnit的原始碼並不是很困難,我相信這與整體架構設計得當有關,使人讀起來神清氣爽。 此文也僅僅是對JUnit的原始碼粗略概括,更多的細節還有待大家仔細琢磨。
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能