Junit源碼與設計模式欣賞

先介紹下這篇博文的由來,之前已經對JUnit的使用經行了深入的介紹和演示(參考JUnit學習(一),JUnit學習(二)),其中的部分功能是通過分析JUnit源代碼找到的。得益於這個過程有幸完整的拜讀了JUnit的源碼十分讚歎作者代碼的精美,一直計劃着把源碼的分析也寫出來。突發奇想決定從設計模式入手賞析JUnit的流程和模式的應用,希望由此能寫出一篇耐讀好看的文章。於是又花了些時日重讀《設計模式》以期能夠順暢的把兩者結合在一起,由於個人水平有限難免出現錯誤、疏漏,還請各位高手多多指出、討論。

測試用例的一生——運行流程圖

這裏寫圖片描述

圖一 TestCase運行時序圖

首先,介紹下JUnit的測試用例運行會經過哪些過程,這裏說起來有些抽象會讓人比較迷惑,在看了後面章節的內容之後就比較清晰了:

Client(JUnitCore、Eclipse):這裏是TestCase開始的地方,如果是Main函數來啓動測試用例的話一般會調用JUnitCore的方法,如果是在Eclipse中使用JUnit插件則實際上調用的是org.eclipse.jdt.internal.junit4.runner.JUnit4TestClassReference這個類。
Request(org.junit.runner.Request):Client會根據測試類或方法而創建一個Request實體,Request包含了要被執行的測試類、測試方法等信息。
RunnerBuilder:在創建後Client調用Request.getRunner()方法獲取用於執行測試的Runner,該過程是由RunnerBuilder這個工廠類完成的。
RunNotifier:在執行Runner.run方法時Client還會傳遞一個RunNotifier對象,是事件的監聽器的管理員。Runner在開始執行、成功、失敗和執行結束時會調用RunNotifier中相應的方法從而發送事件給註冊了的監聽器,JUnit運行的最終結果就是這些監聽器收集展現的。
Runner:從名字可以猜出這裏應該是測試用例運行的地方了,在Client調用了Runner.run()方法之後,Runner會先構造Statement對象將所有要執行的邏輯委託給它,接着執行Statement.evaluate()方法。在這期間Runner會觸發RunNotifier(如測試開始、測試結束等等)。
Statement:測試用例的運行時描述,關注於測試用例如何運行和調用測試代碼。比如在執行測試用例前是否有@Before註釋的方法需要調用等信息都會被Runner構造爲Statement。

JUnit的類設計和模式應用

這裏寫圖片描述

圖二 JUnit的類結構

首先先介紹下JUnit中的模型類(Model),在JUnit模型類可以劃分爲三個範圍:

  • 描述模型:是對要執行的測試用例的描述(比如要執行哪個類的哪個方法,是否有指定Runner,使用了哪些註解等),這一層類似於流程文件之於流程引擎——不是用來執行的,而是描述要有哪些環節、細節。個人認爲這一模型包括測試類本身(即你自己編寫的測試用例)和Request。其中測試類本身等同於描述文件,Request則記錄了是要運行的Suit、測試類或者是某個具體的方法、過濾器、排序的Comparator等信息(JUnit是支持對測試方法排序和過濾的)。

  • 運行時模型:是JUnit中可執行的模型,包括FrameworkMember(org.junit.runners.model.FrameworkMember)及其子類、TestClass(org.junit.runners.model.TestClass)、Statement。FrameworkMember的子類包括FrameworkMethod和FrameworkField分別描述了測試類的方法和變量信息,比如是否爲靜態、作用域、包含哪些註解等JUnit運行時需要用到的信息;TestClass的作用有些類似FrameworkMember,是針對測試的Class的描述。Statement在上面已經介紹過是對測試執行流程和細節的描述。

  • 結果模型:JUnit中用於描述用例的類,包括Description(org.junit.runner.Description)、Result(org.junit.runner.Result)、Failure(org.junit.runner.notification.Failure)。Description是對測試用例的描述(測試名稱、所在Class的名字、是否是suit等等)只爲RunNotifier提供服務。Result是運行結果的描述,用例執行完成後RunNotifier的 fireTestRunFinished(final Result result)方法會被觸發,傳入的Result實例描述了運行耗時、忽略的用例次數、是否成功等信息。Failure則是用例失敗後Runner傳遞給RunNotifier的對象用於描述錯誤信息,特別包含了錯誤的StackTrace。
    言歸正傳,下面討論設計模式和JUnit的源碼:

          工廠方法模式、職責鏈:用例啓動,Client在創建Request後會調用RunnerBuilder(工廠方法的抽象類)來創建Runner,默認的實現是AllDefaultPosibilitiesBuilder,根據不同的測試類定義(@RunWith的信息)返回Runner。AllDefaultPosibilitiesBuilder使用職責鏈模式來創建Runner,部分代碼如下。代碼A是AllDefaultPosibilitiesBuilder的主要構造邏輯構造了一個【IgnoreBuilder->AnnotatedBuilder->SuitMethodBuilder->JUnit3Builder->JUnit4Builder】的職責鏈,構造Runner的過程中有且只有一個handler會響應請求。代碼B是Junit4Builder類實現會返回一個BlockJUnit4ClassRunner對象,這個是JUnit4的默認Runner。
    
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;
    }

代碼A 職責鏈實現

public class JUnit4Builder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        return new BlockJUnit4ClassRunner(testClass);
    }
}

代碼B JUnit4Builder實現

	組合模式:將具備樹形結構的數據抽象出公共的接口,在遍歷的過程中應用同樣的處理方式。這個模式在Runner中的應用不是很明顯,扣進來略有牽強。Runner是分層次的,父層包括@BeforeClass、@AfterClass、@ClassRule註解修飾的方法或變量,它們在測試執行前或執行後執行一次。兒子層是@Before、@After、@Rule修飾的方法或變量它們在每個測試方法執行前後執行。當編寫的用例使用Suit來運行時則是三層結構,上面的父子結構中間插入了一層childrenRunners,也就是一個Suilt中每個測試類都會生成一個Runner,調用順序變成了Runner.run()>childRunner.run()<即遍歷childrenRunners>->testMethod()。ParentRunner中將變化的部分封裝爲runChild()方法交給子類實現,達到了遍歷過程使用同樣處理方式的目的——ParentRunner.this.runChild(each,notifier)。
public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            Statement statement = classBlock(notifier);
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.fireTestIgnored();
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }

    private void runChildren(final RunNotifier notifier) {
        for (final T each : getFilteredChildren()) {
            fScheduler.schedule(new Runnable() {
                public void run() {
                    ParentRunner.this.runChild(each, notifier);
                }
            });
        }
        fScheduler.finished();
    }

代碼C ParentRunner的組合模式應用

 模板方法模式:模板方法的目的是抽取公共部分封裝變化,在父類中會包含公共流程的代碼,將變化的部分封裝爲抽象方法由子類實現(就像模板一樣框架式定好的,你去填寫你需要的內容就行了)。JUnit的默認Runner——BlockJUnit4ClassRunner繼承自ParentRunner,ParentRunner類定義了Statement的構造和執行流程,而如何執行兒子層的runChild方法時交給子類實現的,在BlockJUnit4ClassRunner中就是去構造和運行TestMethod,而另一個子類Suit中則是執行子層次的runner.run。

這裏寫圖片描述
圖三 RunNotifier的公共方法

private abstract class SafeNotifier {
        private final List<RunListener> fCurrentListeners;

        SafeNotifier() {
            this(fListeners);
        }

        SafeNotifier(List<RunListener> currentListeners) {
            fCurrentListeners = currentListeners;
        }

        void run() {
            synchronized (fListeners) {
                List<RunListener> safeListeners = new ArrayList<RunListener>();
                List<Failure> failures = new ArrayList<Failure>();
                for (Iterator<RunListener> all = fCurrentListeners.iterator(); all
                        .hasNext(); ) {
                    try {
                        RunListener listener = all.next();
                        notifyListener(listener);
                        safeListeners.add(listener);
                    } catch (Exception e) {
                        failures.add(new Failure(Description.TEST_MECHANISM, e));
                    }
                }
                fireTestFailures(safeListeners, failures);
            }
        }

        abstract protected void notifyListener(RunListener each) throws Exception;
    }

public void fireTestRunStarted(final Description description) {
        new SafeNotifier() {
            @Override
            protected void notifyListener(RunListener each) throws Exception {
                each.testRunStarted(description);
            }

            ;
        }.run();
    }

代碼D RunNotifier代碼截取

裝飾模式:保持對象原有的接口不改變而透明的增加對象的行爲,看起來像是在原有對象外面包裝了一層(或多層)行爲——雖然對象還是原來的類型但是行爲逐漸豐富起來。 之前一直在強調Statement描述了測試類的執行細節,到底是如何描述的呢?代碼E展示了Statement的構築過程,首先是調用childrenInvoker方法構建了Statement的基本行爲——執行所有的子測試runChildren(notifier)(非Suit情況下就是TestMethod了,如果是Suit的話則是childrenRunners)。
接着是裝飾模式的應用,代碼F是withBeforeClasses()的實現——很簡單,檢查是否使用了@BeforeClasses註解修飾如果存在構造RunBefores對象——RunBefore繼承自Statement。代碼H中的evaluate()方法可以發現新生成的Statement在執行runChildren(fNext.evaluate())之前遍歷所有使用@BeforeClasses註解修飾的方法並執行。產生的效果即使用@BeforeClasses修飾的方法會在所有用例運行前執行且只執行一次。後面的withAfterClasses、withClassRules方法原理一樣都使用了裝飾模式,不再贅述。
protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        return statement;
    }

    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }

代碼E Statement的構造

protected Statement withBeforeClasses(Statement statement) {
        List<FrameworkMethod> befores = fTestClass
                .getAnnotatedMethods(BeforeClass.class);
        return befores.isEmpty() ? statement :
                new RunBefores(statement, befores, null);
    }

代碼F withBeforeClasses實現

public class RunBefores extends Statement {
    private final Statement fNext;

    private final Object fTarget;

    private final List<FrameworkMethod> fBefores;

    public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
        fNext = next;
        fBefores = befores;
        fTarget = target;
    }

    @Override
    public void evaluate() throws Throwable {
        for (FrameworkMethod before : fBefores) {
            before.invokeExplosively(fTarget);
        }
        fNext.evaluate();
    }
}

代碼H RunBefores實現

策略模式:針對相同的行爲在不同場景下算法不同的情況,抽象出接口類,在子類中實現不同的算法並提供算法執行必須Context信息。JUnit中提供了Timeout、ExpectedException、ExternalResource等一系列的TestRule用於豐富測試用例的行爲,這些TestRule的都是通過修飾Statement實現的。
修飾Statement的代碼在withRules()方法中實現,使用了策略模式。代碼I描述了JUnit是如何處理@Rule標籤的,withRules方法獲取到測試類中所有的@Rule修飾的變量,分別調用withMethodRules和withTestRules方法,前者是爲了兼容JUnit3版本的Rule這裏忽略,後者withTestRules的邏輯很簡單首先查看是否使用了@Rule,如存在就交給RunRules類處理。代碼J是RunRules的實現,在構造函數中處理了修飾Statement的邏輯(applyAll方法)——抽象接口是TestRule,根據不同的場景(即使用@Rule修飾的不同的TestRule的實現)選擇不同的策略(TestRule的具體實現),而Context信息就是入參(result:Statement, description:Description)。
private Statement withRules(FrameworkMethod method, Object target,
            Statement statement) {
        List<TestRule> testRules = getTestRules(target);
        Statement result = statement;
        result = withMethodRules(method, testRules, target, result);
        result = withTestRules(method, testRules, result);

        return result;
    }

private Statement withTestRules(FrameworkMethod method, List<TestRule> testRules,
            Statement statement) {
        return testRules.isEmpty() ? statement :
                new RunRules(statement, testRules, describeChild(method));
    }

代碼I rule的執行

public class RunRules extends Statement {
    private final Statement statement;

    public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
        statement = applyAll(base, rules, description);
    }

    @Override
    public void evaluate() throws Throwable {
        statement.evaluate();
    }

    private static Statement applyAll(Statement result, Iterable<TestRule> rules,
            Description description) {
        for (TestRule each : rules) {
            result = each.apply(result, description);
        }
        return result;
    }
}

代碼J RunRules實現

 這裏簡單舉一個Timeout的代碼。Timeout首先實現了TestRule方法並在apply中定義了自己的算法——構造一個FailOnTimeout對象並返回。不再詳細描述FailOnTimeout的實現,主要原理就是起一個線程來跑測試代碼並等待線程指定的時間,如果超時就會拋出異常。
public class Timeout implements TestRule {
    private final int fMillis;

    /**
     * @param millis the millisecond timeout
     */
    public Timeout(int millis) {
        fMillis = millis;
    }

    public Statement apply(Statement base, Description description) {
        return new FailOnTimeout(base, fMillis);
    }
}

代碼K Timeout實現

外觀模式:封裝統一的對外接口,隱藏內部各個小模塊之間的調用細節,使得用戶既可以簡單的使用facade來達到目的,必要時又可以自行操作內部對象。這裏的舉例可能不是非常明顯。圖四是TestClass的公共方法,代碼L給出了 TestClass(org.junit.runners.model.TestClass)的一小部分代碼,TestClass主要作用是封裝對Class的操作提供了JUnit運行時需要用到的功能並隱藏其中操作的細節。在TestClass內部將功能委託給了三個對象Class(java.lang.Class)、FrameworkMethod、FrameworkField來實現,本身充當了外觀(facade)對外提供了getName、getOnlyConstructor、getAnnotations等等接口,在必要的時又可以通過這個外觀獲取到Class、FrameworkMethod等對象。 

這裏寫圖片描述

圖四 TestClass的公共方法

public class TestClass {
    private final Class<?> fClass;

    private Map<Class<?>, List<FrameworkMethod>> fMethodsForAnnotations = new HashMap<Class<?>, List<FrameworkMethod>>();

    private Map<Class<?>, List<FrameworkField>> fFieldsForAnnotations = new HashMap<Class<?>, List<FrameworkField>>();

    /**
     * Creates a {@code TestClass} wrapping {@code klass}. Each time this
     * constructor executes, the class is scanned for annotations, which can be
     * an expensive process (we hope in future JDK's it will not be.) Therefore,
     * try to share instances of {@code TestClass} where possible.
     */
    public TestClass(Class<?> klass) {
        fClass = klass;
        if (klass != null && klass.getConstructors().length > 1) {
            throw new IllegalArgumentException(
                    "Test class can only have one constructor");
        }

        for (Class<?> eachClass : getSuperClasses(fClass)) {
            for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {
                addToAnnotationLists(new FrameworkMethod(eachMethod),
                        fMethodsForAnnotations);
            }
            for (Field eachField : eachClass.getDeclaredFields()) {
                addToAnnotationLists(new FrameworkField(eachField),
                        fFieldsForAnnotations);
            }
        }
    }
    ……
}

代碼L TestClass截取

寫在最後

讀JUnit代碼時確實非常讚歎其合理的封裝和靈活的設計,自己雖然也寫了幾年代碼但是在JUnit的源碼中收穫很多。由於對源碼的鑽研深度以及設計模式的領會不夠深入,文中有很多牽強和錯誤的地方歡迎大家討論指正。最喜歡的是JUnit對裝飾模式和職責鏈的應用,在看到AllDefaultPossiblitiesBuilder中對職責鏈的應用還覺得設計比較合理,等到看到Statement的創建和組裝就感慨設計的精湛了,無論是基本的調用測試方法的邏輯還是@Before、@After等以及實現自TestRule的邏輯一併融入到Statement的構造中,又不會牽扯出太多的耦合。總之無論是設計模式還是設計思想,歸根結底就是抽取公共部分,封裝變化,做到靈活、解耦。最後說明這篇文章根據的源代碼是JUnit4.11的,maven座標如下,在JUnit的其它版本中源碼差別比較大沒有研究過。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency>

轉自:junit源碼與設計模式欣賞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章