天天看點

深入JUnit源碼之Rule 深入JUnit源碼之Rule

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

junit中的rule是對@beforeclass、@afterclass、@before、@after等注解的另一種實作,其中@classrule實作的功能和@beforeclass、@afterclass類似;@rule實作的功能和@before、@after類似。junit引入@classrule和@rule注解的關鍵是想讓以前在@beforeclass、@afterclass、@before、@after中的邏輯能更加友善的實作重用,因為@beforeclass、@afterclass、@before、@after是将邏輯封裝在一個測試類的方法中的,如果實作重用,需要自己将這些邏輯提取到一個單獨的類中,再在這些方法中調用,而@classrule、@rule則是将邏輯封裝在一個類中,當需要使用時,直接指派即可,對不需要重用的邏輯則可用匿名類實作,也是以,junit在接下來的版本中更傾向于多用@classrule和@rule,雖然就我自己來說,感覺還是用@beforeclass、@afterclass、@before、@after這些注解更加熟悉一些,也可能是我測試代碼寫的還不夠多的原因吧l。同時由于statement鍊構造的特殊性@classrule或@rule也保證了類似父類@beforeclass或@before注解的方法要比子類的注解方法執行早,而父類的@afterclass或@after注解的方法執行要比子類要早的特點。

@classrule和@rule隻能注解在字段中,并且該字段的類型必須實作了testrule接口,對@classrule注解的字段還必須是public,static,并且@classrule注解的字段在運作時不可以抛異常,不然junit的行為是未定義的,這個是注釋文檔中這樣描述的,實際情況則一般是直接觸發testfailure事件,至于其他結果,則要看不同的testrule實作不同,這個特征将在下面詳細講解;而對@rule注解的字段必須是public,非static,關于@classrule注解字段和@rule注解字段的驗證是在rulefieldvalidator中做的(具體可以參考runner小節):

 1 public enum rulefieldvalidator {

 2     class_rule_validator(classrule.class, true), rule_validator(rule.class, false);

 3     

深入JUnit源碼之Rule 深入JUnit源碼之Rule

 4     public void validate(testclass target, list<throwable> errors) {

 5        list<frameworkfield> fields= target.getannotatedfields(fannotation);

 6        for (frameworkfield each : fields)

 7            validatefield(each, errors);

 8     }

 9     private void validatefield(frameworkfield field, list<throwable> errors) {

10        optionallyvalidatestatic(field, errors);

11        validatepublic(field, errors);

12        validatetestruleormethodrule(field, errors);

13     }

14     private void optionallyvalidatestatic(frameworkfield field,

15            list<throwable> errors) {

16        if (fonlystaticfields && !field.isstatic())

17            adderror(errors, field, "must be static.");

18     }

19     private void validatepublic(frameworkfield field, list<throwable> errors) {

20        if (!field.ispublic())

21            adderror(errors, field, "must be public.");

22     }

23     private void validatetestruleormethodrule(frameworkfield field,

24            list<throwable> errors) {

25        if (!ismethodrule(field) && !istestrule(field))

26            adderror(errors, field, "must implement methodrule or testrule.");

27     }

28     private boolean istestrule(frameworkfield target) {

29        return testrule.class.isassignablefrom(target.gettype());

30     }

31     private boolean ismethodrule(frameworkfield target) {

32        return org.junit.rules.methodrule.class.isassignablefrom(target

33               .gettype());

34     }

35     private void adderror(list<throwable> errors, frameworkfield field,

36            string suffix) {

37        string message= "the @" + fannotation.getsimplename() + " '"

38               + field.getname() + "' " + suffix;

39        errors.add(new exception(message));

40     }

41 }

本節将重點介紹目前junit預設實作的幾個testrule,先給出類圖,然後介紹源碼實作以及用途,最後還将簡單的介紹runrules這個statement的運作資訊,雖然這個類非常簡單,在statement那節中也已經簡單的做過介紹了。

在學一個新的架構的時候,我一直比較喜歡先看一下架構的類圖,這樣自己總體上就有個概念了。這裡也先給一張junit中testrule的類圖吧:

深入JUnit源碼之Rule 深入JUnit源碼之Rule

testrule的類結構圖還是比較簡單的,隻是将它置于junit的statement架構中,有些問題分析起來就比較複雜了。為了保持問題的簡單,我們先來看一下每個單獨的類各自實作了什麼功能和怎麼實作吧。

先來看兩個簡單的吧,testwatcher為子類提供了四個事件方法以監控測試方法在運作過程中的狀态,一般它可以作為資訊記錄使用。如果testwatcher作為@classrule注解字段,則該測試類在運作之前(調用所有的@beforeclass注解方法之前)會調用starting()方法;當所有@afterclass注解方法調用結束後,succeeded()方法會被調用;若@afterclass注解方法中出現異常,則failed()方法會被調用;最後,finished()方法會被調用;所有這些方法的description是runner對應的description。如果testwatcher作為@rule注解字段,則在每個測試方法運作前(所有的@before注解方法運作前)會調用starting()方法;當所有@after注解方法調用結束後,succeeded()方法會被調用;若@after注解方法中跑出異常,則failed()方法會被調用;最後,finished()方法會被調用;所有description的執行個體是測試方法的description執行個體。

testname是對testwatcher的一個簡單實作,它會在starting()方法中記錄每次運作的名字。如果testname作為@rule注解字段,則starting()中傳入的description是對每個測試方法的description,因而getmethodname()方法傳回的是測試方法的名字。一般testname不作為@classrule注解字段,如果真有人這樣用了,則starting()中description的參數是runner的description執行個體,一般getmethodname()傳回值為null。

 1 public abstract class testwatcher implements testrule {

 2     public statement apply(final statement base, final description description) {

 3        return new statement() {

 4            @override

 5            public void evaluate() throws throwable {

 6               starting(description);

 7               try {

 8                   base.evaluate();

 9                   succeeded(description);

10               } catch (assumptionviolatedexception e) {

11                   throw e;

12               } catch (throwable t) {

13                   failed(t, description);

14                   throw t;

15               } finally {

16                   finished(description);

17               }

18            }

19        };

20     }

21     protected void succeeded(description description) {

23     protected void failed(throwable e, description description) {

24     }

25     protected void starting(description description) {

26     }

27     protected void finished(description description) {

28     }

29 }

30 public class testname extends testwatcher {

31     private string fname;

32     @override

33     protected void starting(description d) {

34        fname= d.getmethodname();

35     }

36     public string getmethodname() {

37        return fname;

38     }

39 }

temporaryfolder是對externalresource的一個實作,它在before()方法中在臨時檔案夾中建立一個随機的檔案夾,以junit開頭;并在after()方法将建立的臨時檔案夾清空,并删除該臨時檔案夾。另外temporaryfolder還提供了幾個方法以在新建立的臨時檔案夾中建立新的檔案、檔案夾。

 1 public abstract class externalresource implements testrule {

 2     public statement apply(statement base, description description) {

 3        return statement(base);

 4     }

 5     private statement statement(final statement base) {

 6        return new statement() {

 7            @override

 8            public void evaluate() throws throwable {

 9               before();

10               try {

11                   base.evaluate();

12               } finally {

13                   after();

14               }

15            }

16        };

17     }

18     protected void before() throws throwable {

19     }

20     protected void after() {

21     }

22 }

23 public class temporaryfolder extends externalresource {

24     private file folder;

25     @override

26     protected void before() throws throwable {

27        create();

29     @override

30     protected void after() {

31        delete();

32     }

33     public void create() throws ioexception {

34        folder= newfolder();

36     public file newfile(string filename) throws ioexception {

37        file file= new file(getroot(), filename);

38        file.createnewfile();

39        return file;

41     public file newfile() throws ioexception {

42        return file.createtempfile("junit", null, folder);

43     }

44     public file newfolder(string

深入JUnit源碼之Rule 深入JUnit源碼之Rule

 foldernames) {

45        file file = getroot();

46        for (string foldername : foldernames) {

47            file = new file(file, foldername);

48            file.mkdir();

49        }

50        return file;

51     }

52     public file newfolder() throws ioexception {

53        file createdfolder= file.createtempfile("junit", "", folder);

54        createdfolder.delete();

55        createdfolder.mkdir();

56        return createdfolder;

57     }

58     public file getroot() {

59        if (folder == null) {

60            throw new illegalstateexception("the temporary folder has not yet been created");

61        }

62        return folder;

63     }

64     public void delete() {

65        recursivedelete(folder);

66     }

67     private void recursivedelete(file file) {

68        file[] files= file.listfiles();

69        if (files != null)

70            for (file each : files)

71               recursivedelete(each);

72        file.delete();

73     }

74 }

verifier是在所有測試已經結束的時候,再加入一些額外的邏輯,如果額外的邏輯通過,才表示測試成功,否則,測試依舊失敗,即使在之前的運作中都是成功的。verify可以為一些很多測試方法加入一些公共的驗證邏輯。當verifier應用在@rule注解字段中,它在所偶@after注解方法運作完後,會調用verify()方法,如果verifier()方法驗證失敗抛出異常,則該測試方法的testfailure事件将會被觸發,導緻該測試方法失敗;當verifier應用在@classrule時,它在所有的@afterclass注解的方法執行完後,會執行verify()方法,如果verify失敗抛出異常,将會觸發關于該測試類的testfailure,此時測試類中的所有測試方法都已經運作成功了,卻在最後收到一個關于測試類的testfailure事件,這确實是一個比較詭異的事情,因而@classrule中提到errorcollector(verifier)不可以用在@classrule注解中,否則其行為為定義;更一般的@classrule注解的字段運作時不能抛異常,不然其行為是未定義的。

errorcollector是對verifier的一個實作,它可以在運作測試方法的過程中收集錯誤資訊,而這些錯誤資訊知道最後調用errorcollector的verify()方法時再處理。其實就目前來看,我很難想象這個需求存在的意義,因為即使它将所有的錯誤資訊收集在一起了,在事件釋出是,它還是會為每個錯誤釋出一次testfailure事件(參考eachtestnotifier的實作),除非有一種需求是即使測試方法在運作過程的某個點運作出錯,也隻是先記錄這個錯誤,等到所有邏輯運作結束後才去将這個測試方法運作過程中存在的錯誤釋出出去,這樣一次運作就可以知道測試代碼中存在出錯的地方。errorcollector中還提供了幾個收集錯誤的方法:如adderror()、checkthat()、checksucceeds()等。這裡的checkthat()方法用到了hamcrest架構中的matcher,這部分的内容将在assert小節中詳細介紹。

 1 public class verifier implements testrule {

 2     public statement apply(final statement base, description description) {

 6               base.evaluate();

 7               verify();

 8            }

 9        };

10     }

11     protected void verify() throws throwable {

12     }

13 }

14 public class errorcollector extends verifier {

15     private list<throwable> errors= new arraylist<throwable>();

16     @override

17     protected void verify() throws throwable {

18        multiplefailureexception.assertempty(errors);

20     public void adderror(throwable error) {

21        errors.add(error);

23     public <t> void checkthat(final t value, final matcher<t> matcher) {

24        checkthat("", value, matcher);

25     }

26     public <t> void checkthat(final string reason, final t value, final matcher<t> matcher) {

27        checksucceeds(new callable<object>() {

28            public object call() throws exception {

29               assertthat(reason, value, matcher);

30               return value;

31            }

32        });

33     }

34     public object checksucceeds(callable<object> callable) {

35        try {

36            return callable.call();

37        } catch (throwable e) {

38            adderror(e);

39            return null;

40        }

41     }

42 }

 1 public class timeout implements testrule {

 2     private final int fmillis;

 3     public timeout(int millis) {

 4        fmillis= millis;

 5     }

 6     public statement apply(statement base, description description) {

 7        return new failontimeout(base, fmillis);

 9 }

10 public class expectedexception implements testrule {

11     public static expectedexception none() {

12        return new expectedexception();

14     private matcher<object> fmatcher= null;

15     private expectedexception() {

16     }

17     public statement apply(statement base,

18            org.junit.runner.description description) {

19        return new expectedexceptionstatement(base);

21     public void expect(matcher<?> matcher) {

22        if (fmatcher == null)

23            fmatcher= (matcher<object>) matcher;

24        else

25            fmatcher= both(fmatcher).and(matcher);

27     public void expect(class<? extends throwable> type) {

28        expect(instanceof(type));

29     }

30     public void expectmessage(string substring) {

31        expectmessage(containsstring(substring));

33     public void expectmessage(matcher<string> matcher) {

34        expect(hasmessage(matcher));

36     private class expectedexceptionstatement extends statement {

37        private final statement fnext;

38        public expectedexceptionstatement(statement base) {

39            fnext= base;

41        @override

42        public void evaluate() throws throwable {

43            try {

44               fnext.evaluate();

45            } catch (throwable e) {

46               if (fmatcher == null)

47                   throw e;

48               assert.assertthat(e, fmatcher);

49               return;

50            }

51            if (fmatcher != null)

52               throw new assertionerror("expected test to throw "

53                      + stringdescription.tostring(fmatcher));

54        }

55     }

56     private matcher<throwable> hasmessage(final matcher<string> matcher) {

57        return new typesafematcher<throwable>() {

58            public void describeto(description description) {

59               description.appendtext("exception with message ");

60               description.appenddescriptionof(matcher);

61            }

62            @override

63            public boolean matchessafely(throwable item) {

64               return matcher.matches(item.getmessage());

65            }

66        };

67     }

68 }

rulechain提供一種将多個testrule串在一起執行的機制,它首先從outchain()方法開始建立一個最外層的testrule建立的statement,而後調用round()方法,不斷向内層添加testrule建立的statement。如其注釋文檔中給出的一個例子:

1 @rule

2 public testrule chain= rulechain

3                     .outerrule(new loggingrule("outer rule"))

4                     .around(new loggingrule("middle rule"))

5                     .around(new loggingrule("inner rule"));

如果loggingrule隻是類似externalresource中的實作,并且在before()方法中列印starting…,在after()方法中列印finished…,那麼這條鍊的執行結果為:

starting outer rule

starting middle rule

starting inner rule

finished inner rule

finished middle rule

finished outer rule

由于testrule的apply()方法是根據的目前傳入的statement,建立一個新的statement,以決定目前testrule邏輯的執行位置,因而第一個調用apply()的testrule産生的statement将在statement鍊的最裡面,也正是有這樣的邏輯,是以around()方法實作的時候,都是把新加入的testrule放在第一個位置,然後才保持其他已存在的testrule位置不變。

 1 public class rulechain implements testrule {

 2     private static final rulechain empty_chain= new rulechain(

 3            collections.<testrule> emptylist());

 4     private list<testrule> rulesstartingwithinnermost;

 5     public static rulechain emptyrulechain() {

 6        return empty_chain;

 7     }

 8     public static rulechain outerrule(testrule outerrule) {

 9        return emptyrulechain().around(outerrule);

11     private rulechain(list<testrule> rules) {

12        this.rulesstartingwithinnermost= rules;

14     public rulechain around(testrule enclosedrule) {

15        list<testrule> rulesofnewchain= new arraylist<testrule>();

16        rulesofnewchain.add(enclosedrule);

17        rulesofnewchain.addall(rulesstartingwithinnermost);

18        return new rulechain(rulesofnewchain);

20     public statement apply(statement base, description description) {

21        for (testrule each : rulesstartingwithinnermost)

22            base= each.apply(base, description);

23        return base;

25 }

testrule執行個體的運作都是被封裝在一個叫runrules的statement中運作的。在構造runrules執行個體是,傳入testrule執行個體的集合,然後周遊所有的testrule執行個體,為每個testrule執行個體調用一遍apply()方法以構造出要執行testrule的statement鍊。類似上小節的rulechain,這裡在前面的testrule構造的statement被是最終構造出的statement的最裡層,結合testclass在擷取注解字段的順序時,先查找子類,再查找父類,因而子類的testrule執行個體産生的statement是在statement鍊的最裡層,進而保證了類似externalresource實作中,before()方法的執行父類要比子類要早,而after()方法的執行子類要比父類要早的特性。

 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);

 6     @override

 7     public void evaluate() throws throwable {

 8        statement.evaluate();

 9     }

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;

15     }

16 }