歡迎光臨
每天分享高質量文章

JUnit 原始碼解析

(點選上方公眾號,可快速關註)


來源: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技能

贊(0)

分享創造快樂