天天看點

深入JUnit源碼之Statement深入JUnit源碼之Statement

初次用文字的方式記錄讀源碼的過程,不知道怎麼寫,感覺有點貼代碼的嫌疑。不過中間還是加入了一些自己的了解和心得,希望以後能夠慢慢的改進,感興趣的童鞋湊合着看吧,感覺junit這個架構還是值得看的,裡面有許多不錯的設計思想在,更何況它是kent beck和erich gamma這樣的大師寫的。。。。。

看junit源碼最大的收獲就是看到這個statement的設計,它也是我看到過的所有源碼中最喜歡的設計之一。junit中runner的運作過程就是statement鍊的運作過程,statement是對一個單元運作的封裝,每個statement都隻是執行它本身所表達的邏輯,而将其他邏輯交給下一個statement處理,而且基本上的statement都存在對下一個節點的引用,進而由此構成一條statement的鍊,從設計模式的角度上來看,這是一個職責連模式(chain of responsibility pattern)。junit中對@beforeclass、@afterclass、@before、@after、@classrule、@rule等邏輯就是通過statement來實作的。首先來看一下statement的類結構。

深入JUnit源碼之Statement深入JUnit源碼之Statement

statement的類結構還是比較簡單的,首先statement是所有類的父類,它隻定義了一個抽象的evaluate()方法,由其他子類實作該方法;而且除了fail和invokemethod類,其他類都有一個對statement本身的引用。其實從實作上,每個statement也是比較簡單的,這個接下來就可以看到了。每個statement都隻實作它自己的邏輯,而将其他邏輯代理給另一個statement執行,這樣可以在編寫每個statement的時候隻關注自己的邏輯,進而保持statement本身的簡單,并且易于擴充,當一條statement執行完後,整個邏輯也就執行完了。不過statement這條鍊也不是憑空産生的,它也是要根據一定的邏輯構造起來的,關于statement鍊的構造在junit中由runner負責,為了保持本文的完整性,本文會首先會講解上述幾個statement的源碼,同時簡單回顧statement鍊的構造過程,最後将通過一個簡單的例子,将statement的執行過程用序列圖的方式表達出來,以更加清晰的表達statement的執行過程。不過本文不會詳細介紹rules相關的代碼,這部分的代碼将會在下一節詳細介紹。

這兩個statement是針對junit中@beforeclass、@before的實作的,其中@beforeclass是在測試類運作時,所有測試方法運作之前運作,并且對每個測試類隻運作一次,這個注解修飾的方法必須是靜态的(在runner一節中有談過它為什麼要被設計成一定是要靜态方法,因為在運作每個測試方法是,測試類都會從新初始化一遍,如果不是靜态類,它隻運作一次的話,它運作的結果無法儲存下來);@before是在每個測試方法運作之前都要運作。

在statement的設計中,@beforeclass注解的方法抽象成一個statement叫runbefores,而測試類中其他要運作的測試方法的運作過程是另一個statement叫next,在runbefores中調用完所有這些方法,而将其他邏輯交給next; @before注解的方法也是一樣的邏輯,它把接下來的測試方法看成是一個statement叫next,它調用完所有@before注解的方法後,将接下來的事情交給next,因而他們共享runbefores的statement,唯一不同的是@beforeclass的runbefores可以直接調用測試類中的方法,因為他們是靜态的,而@before的runbefores需要傳入測試類的執行個體。

 1 public class runbefores extends statement {

 2     private final statement fnext;

 3     private final object ftarget;

 4     private final list<frameworkmethod> fbefores;

 5     public runbefores(statement next, list<frameworkmethod> befores, object target) {

 6        fnext= next;

 7        fbefores= befores;

 8        ftarget= target;

 9     }

10     @override

11     public void evaluate() throws throwable {

12        for (frameworkmethod before : fbefores)

13            before.invokeexplosively(ftarget);

14        fnext.evaluate();

15     }

16 }

從源碼中可以看到,構造runbefores時傳入下一個statement、所有@beforeclass或@before注解的方法以及測試類的執行個體,對@beforeclass來說,傳入null即可。在運作evaluate()方法時,它依次調用@beforeclass或@before注解的方法,後将接下來的邏輯交給next。從這段邏輯也可以看出如果有一個@beforeclass或@before注解的方法抛異常,接下來的這些方法就都不會執行了,包括測試方法。不過此時@afterclass或@after注解的方法還會執行,這個在下一小節中即可知道。

關于runbefores的構造要,其實最重要的是要關注它的next statement是什麼,對于@beforeclass對應的runbefores來說,它的next statement那些所有的測試方法運作而組成的statement,而對于@before對應的runbefores來說,它的next statement是測試方法的statement:

 1 protected statement classblock(final runnotifier notifier) {

 2     statement statement= childreninvoker(notifier);

 3     statement= withbeforeclasses(statement);

 4     

深入JUnit源碼之Statement深入JUnit源碼之Statement

 5 }

 6 protected statement withbeforeclasses(statement statement) {

 7     list<frameworkmethod> befores= ftestclass

 8            .getannotatedmethods(beforeclass.class);

 9     return befores.isempty() ? statement :

10        new runbefores(statement, befores, null);

11 }

12 protected statement methodblock(frameworkmethod method) {

13     

深入JUnit源碼之Statement深入JUnit源碼之Statement

14     statement statement= methodinvoker(method, test);

15     

深入JUnit源碼之Statement深入JUnit源碼之Statement

16     statement= withbefores(method, test, statement);

17     

深入JUnit源碼之Statement深入JUnit源碼之Statement

18 }

19 protected statement withbefores(frameworkmethod method, object target,

20        statement statement) {

21     list<frameworkmethod> befores= gettestclass().getannotatedmethods(

22            before.class);

23     return befores.isempty() ? statement : new runbefores(statement,

24            befores, target);

25 }

這兩個statement是針對junit中@afterclass、@after的實作的,其中@afterclass是在測試類運作時,所有測試方法結束之後運作,不管之前的方法是否有抛異常,并且對每個測試類隻運作一次,這個注解修飾的方法必須是靜态的;@after是在每個測試方法運作結束後都要運作的,不管測試方法是否測試失敗。

在statement的設計中,@afterclass注解的方法抽象成一個statement叫afters,而測試類中之前要運作的過程是另一個statement叫next(其實這個叫before更好一些),在runafters中等所有之前的運作過程調用完後,再調用@afterclass注解的方法; @after注解的方法也是一樣的邏輯,它把之前的測試方法包括@before注解的方法看成是一個statement叫next(before?),它等測試方法或@before注解的方法調用完後,調用@after注解的方法,因而他們共享runafters的statement,唯一不同的是@afterclass的runafters可以直接調用測試類中的方法,因為他們是靜态的,而@after的runafters需要傳入測試類的執行個體。

 1 public class runafters extends statement {

 4     private final list<frameworkmethod> fafters;

 5     public runafters(statement next, list<frameworkmethod> afters, object target) {

 7        fafters= afters;

12        list<throwable> errors = new arraylist<throwable>();

13        try {

14            fnext.evaluate();

15        } catch (throwable e) {

16            errors.add(e);

17        } finally {

18            for (frameworkmethod each : fafters)

19               try {

20                   each.invokeexplosively(ftarget);

21               } catch (throwable e) {

22                   errors.add(e);

23               }

24        }

25        multiplefailureexception.assertempty(errors);

26     }

27 }

從源碼中可以看到,構造runafters時傳入下一個statement、所有@afterclass或@after注解的方法以及測試類的執行個體,對@afterclass來說,傳入null即可。在運作evaluate()方法時,它會等之前的statement執行結束後,再依次調用@afterclass或@after注解的方法。從這段邏輯也可以看出無論之前statement執行是否抛異常,@afterclass或@after注解的方法都是會被執行的,為了避免在執行@afterclass或@after注解的方法抛出的異常覆寫之前在運作@beforeclass、@before或@test注解方法抛出的異常,這裡所有的異常都會觸發一次testfailure的事件,這個實作可以檢視runner小節的eachtestnotifier類的實作。

對于runafters的構造,可能要注意的一點是傳入runafters的statement是runbefores的執行個體,這個其實還是好了解的,因為runafters是在傳入的statement運作結束後運作,而runbefores又是要在測試方法之前運作的,因而需要将runafters放在statement鍊的最頭端,而後是runafters,最後才是測試方法調用的statement(invokemethod)。

 2     

深入JUnit源碼之Statement深入JUnit源碼之Statement

 4     statement= withbeforeclasses(statement);

 5     

深入JUnit源碼之Statement深入JUnit源碼之Statement

 6 }

 7 protected statement withafterclasses(statement statement) {

 8     list<frameworkmethod> afters= ftestclass

 9            .getannotatedmethods(afterclass.class);

10     return afters.isempty() ? statement :

11        new runafters(statement, afters, null);

12 }

13 protected statement methodblock(frameworkmethod method) {

14     

深入JUnit源碼之Statement深入JUnit源碼之Statement

15     statement= withbefores(method, test, statement);

16     statement= withafters(method, test, statement);

17     

深入JUnit源碼之Statement深入JUnit源碼之Statement

19 protected statement withafters(frameworkmethod method, object target,

21     list<frameworkmethod> afters= gettestclass().getannotatedmethods(

22            after.class);

23     return afters.isempty() ? statement : new runafters(statement, afters,

24            target);

25 }  

之所有要把這三個statement放在一起是因為他們都是和@test注解相關的:

1 @retention(retentionpolicy.runtime)

2 @target({elementtype.method})

3 public @interface test {

4     class<? extends throwable> expected() default none.class;

5     long timeout() default 0l;

6 }

@test注解定義了兩個成員:expected指定目前測試方法如果抛出指定的異常則表明測試成功;而timeout指定目前測試方法如果超出指定的時間(以毫秒為機關),則測試失敗。在statement設計中,這些邏輯都抽象成了一個statement。而@test注解的方法則被認為是真正要運作的測試方法,它的執行過程也被抽象成了一個statement。

@test注解的方法抽象出的statement命名為invokemethod,它是一個非常簡單的statement:

 1 public class invokemethod extends statement {

 2     private final frameworkmethod ftestmethod;

 3     private object ftarget;

 4     public invokemethod(frameworkmethod testmethod, object target) {

 5        ftestmethod= testmethod;

 6        ftarget= target;

 7     }

 8     @override

 9     public void evaluate() throws throwable {

10        ftestmethod.invokeexplosively(ftarget);

11     }

使用一個方法執行個體和測試類的執行個體構造invokemethod,在運作時直接調用該方法。并且invokemethod并沒有對其他statement的引用,因而它是statement鍊上的最後一個節點。

 1 protected statement methodblock(frameworkmethod method) {

深入JUnit源碼之Statement深入JUnit源碼之Statement

 3     statement statement= methodinvoker(method, test);

 4     statement= possiblyexpectingexceptions(method, test, statement);

 5     statement= withpotentialtimeout(method, test, statement);

 6     

深入JUnit源碼之Statement深入JUnit源碼之Statement

 7 }

 8 protected statement methodinvoker(frameworkmethod method, object test) {

 9     return new invokemethod(method, test);

10 }

expectexception用于處理當在@test注解中定義了expected字段時,該注解所在的方法是否在運作過程中真的抛出了指定的異常,如果沒有,則表明測試失敗,因而它需要該測試方法對應的statement(invokemethod)的引用:

 1 public class expectexception extends statement {

 2     private statement fnext;

 3     private final class<? extends throwable> fexpected;

 4     public expectexception(statement next, class<? extends throwable> expected) {

 5        fnext= next;

 6        fexpected= expected;

 9     public void evaluate() throws exception {

10        boolean complete = false;

11        try {

12            fnext.evaluate();

13            complete = true;

14        } catch (assumptionviolatedexception e) {

15            throw e;

16        } catch (throwable e) {

17            if (!fexpected.isassignablefrom(e.getclass())) {

18               string message= "unexpected exception, expected<"

19                          + fexpected.getname() + "> but was<"

20                          + e.getclass().getname() + ">";

21               throw new exception(message, e);

22            }

23        }

24        if (complete)

25            throw new assertionerror("expected exception: "

26                   + fexpected.getname());

27     }

28 }

使用invokemethod執行個體和一個expected的throwable class執行個體作為參數構造expectexception,當invokemethod執行個體執行後,判斷其抛出的異常是否為指定的異常或者該測試方法沒有抛出異常,在這兩種情況下,測試都會失敗,因而需要它抛出異常以處理這種情況。

深入JUnit源碼之Statement深入JUnit源碼之Statement
深入JUnit源碼之Statement深入JUnit源碼之Statement

 7 protected statement possiblyexpectingexceptions(frameworkmethod method,

 8        object test, statement next) {

 9     test annotation= method.getannotation(test.class);

10     return expectsexception(annotation) ? new expectexception(next,

11            getexpectedexception(annotation)) : next;

failontimeout是在@test注解中指定了timeout值時,用于控制@test注解所在方法的執行時間是否超出了timeout的值,若是,則抛出異常,表明測試失敗。在junit4目前的實作中,它引用的statement執行個體是expectexception(如果expected字段定義了的話)或invokemethod。它通過将引用的statement執行個體的執行放到另一個線程中,然後通過控制線程的執行時間以控制内部引用的statement執行個體的執行時間,如果測試方法因内部抛出異常而沒有完成,則抛出内部抛出的異常執行個體,否則抛出執行時間逾時相關的異常。

 1 public class failontimeout extends statement {

 2     private final statement foriginalstatement;

 3     private final long ftimeout;

 4     public failontimeout(statement originalstatement, long timeout) {

 5        foriginalstatement= originalstatement;

 6        ftimeout= timeout;

10        statementthread thread= evaluatestatement();

11        if (!thread.ffinished)

12            throwexceptionforunfinishedthread(thread);

13     }

14     private statementthread evaluatestatement() throws interruptedexception {

15        statementthread thread= new statementthread(foriginalstatement);

16        thread.start();

17        thread.join(ftimeout);

18        thread.interrupt();

19        return thread;

20     }

21     private void throwexceptionforunfinishedthread(statementthread thread)

22            throws throwable {

23        if (thread.fexceptionthrownbyoriginalstatement != null)

24            throw thread.fexceptionthrownbyoriginalstatement;

25        else

26            throwtimeoutexception(thread);

28     private void throwtimeoutexception(statementthread thread) throws exception {

29        exception exception= new exception(string.format(

30               "test timed out after %d milliseconds", ftimeout));

31        exception.setstacktrace(thread.getstacktrace());

32        throw exception;

33     }

34     private static class statementthread extends thread {

35        private final statement fstatement;

36        private boolean ffinished= false;

37        private throwable fexceptionthrownbyoriginalstatement= null;

38        public statementthread(statement statement) {

39            fstatement= statement;

40        }

41        @override

42        public void run() {

43            try {

44               fstatement.evaluate();

45               ffinished= true;

46            } catch (interruptedexception e) {

47               //don't log the interruptedexception

48            } catch (throwable e) {

49               fexceptionthrownbyoriginalstatement= e;

50            }

51        }

52     }

53 }

failontimeout的構造過程如同上述的其他statement類似:

深入JUnit源碼之Statement深入JUnit源碼之Statement
深入JUnit源碼之Statement深入JUnit源碼之Statement

 8 protected statement withpotentialtimeout(frameworkmethod method,

 9        object test, statement next) {

10     long timeout= gettimeout(method.getannotation(test.class));

11     return timeout > 0 ? new failontimeout(next, timeout) : next;

fail這個statement是在建立測試類出錯時構造的statement,這個設計思想有點類似null object模式,即保持程式設計模型的統一,即使在出錯的時候也用一個statement去封裝,這也正是runner中errorreportingrunner的設計思想。

fail這個statement很簡單,它在運作時重新抛出之前記錄的異常,其構造過程也是在建立測試類執行個體出錯時構造:

 2     object test;

 3     try {

 4        test= new reflectivecallable() {

 5            @override

 6            protected object runreflectivecall() throws throwable {

 7               return createtest();

 8            }

 9        }.run();

10     } catch (throwable e) {

11        return new fail(e);

12     }

深入JUnit源碼之Statement深入JUnit源碼之Statement

14 }

15 public class fail extends statement {

16     private final throwable ferror;

17     public fail(throwable e) {

18        ferror= e;

19     }

20     @override

21     public void evaluate() throws throwable {

22        throw ferror;

23     }

24 }

runrules這個statement是對@classrule和@rule運作的封裝,它會将定義的所有rule應用到傳入的statement引用後傳回,由于它内部還有一些比較複雜的邏輯,關于rule将有一個單獨的小節講解。這裡主要關注runrules這個statement的實作和構造:

 1 public class runrules extends statement {

 2     private final statement statement;

 3     public runrules(statement base, iterable<testrule> rules, description description) {

 4        statement= applyall(base, rules, description);

 5     }

 6     @override

 7     public void evaluate() throws throwable {

 8        statement.evaluate();

10     private static statement applyall(statement result, iterable<testrule> rules,

11            description description) {

12        for (testrule each : rules)

13            result= each.apply(result, description);

14        return result;

17 protected statement classblock(final runnotifier notifier) {

18     

深入JUnit源碼之Statement深入JUnit源碼之Statement

19     statement= withafterclasses(statement);

20     statement= withclassrules(statement);

21     return statement;

22 }

23 private statement withclassrules(statement statement) {

24     list<testrule> classrules= classrules();

25     return classrules.isempty() ? statement :

26         new runrules(statement, classrules, getdescription());

28 protected statement methodblock(frameworkmethod method) {

29     

深入JUnit源碼之Statement深入JUnit源碼之Statement

30     statement= withrules(method, test, statement);

31     return statement;

32 }

33 private statement withrules(frameworkmethod method, object target,

34        statement statement) {

35     list<testrule> testrules= gettestclass().getannotatedfieldvalues(

36         target, rule.class, testrule.class);

37     return testrules.isempty() ? statement :

38        new runrules(statement, testrules, describechild(method));

39 }

rule分為@classrule和@rule,@classrule是測試類級别的,它對一個測試類隻運作一次,而@rule是測試方法級别的,它在每個測試方法運作時都會運作。runrules的構造過程中,我們可以發現runrules才是最外層的statement,testrule中要執行的邏輯要麼比@beforeclass、@before注解的方法要早,要麼比@afterclass、@after注解的方法要遲。

假設在一個測試類中存在多個@beforeclass、@afterclass、@before、@after注解的方法,并且有兩個測試方法testmethod1()和testmethod2(),那麼他們的運作序列圖如下所示(這裡沒有考慮rule,對@classrule的runrules,它的鍊在最前端,而@rule的runrules則是在runafters的前面,關于rule将會在下一節中詳細講解):

深入JUnit源碼之Statement深入JUnit源碼之Statement

繼續閱讀