天天看點

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

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

不知道是因為第一份工作的影響還是受在部落格園上看到的那句“源代碼裡沒有秘密”的影響,總之,近來對很多架構的源碼都很感興趣,拿到一個都想看看。其實自從學習java以來也看過不少了,從剛開始接觸的tomcat,到struts2,再到入職目前這份工作後看的log4j,commons validator和springbatch,可惜除了commons validator的源碼都看完了,其他的架構源碼都半途而廢了,并且commons validator看完後,也沒有什麼記錄下來,是以基本上都可以忽略,其實當時也知道看完要有總結和記錄才會有真正的收獲,然而還是因為各種事情而沒有做。是以這次看junit,發奮看完後一定要做總結,并記錄。

對于junit,一直以為很簡單,可以很快的看完,就當練練手,然而當我真正看完後,才發現junit其實并不簡單,内部架構提供了很多特性,平時都沒用過,而且也沒見過,比如使用runwith注解以指定特定的runner、rule、使用hamcrest架構的assert.assertthat()方法等。其實junit提供的這些特性在一些特殊場合很有用處,然而對于單元測試,很多人卻因為太忙或者因為嫌煩,維護麻煩等各種理由而盡量避免。我也是一樣,雖然我一直承認單元測試非常重要,它不僅僅可以在前期影響設計、而且可以為後期的一些重構或者bug修複提供信心,但是要我真正的認真的為一個子產品寫單元測試,也感覺有點厭煩,再加上目前項目又是一個很少寫單元測試的環境,對這方面堅持就松懈了。

後來,想系統的看一下項目的源碼,可是代碼量太多,也沒有比較明确的子產品分工,沒有比較完善的單元測試,再加上目前項目主要是基于檔案對資料的處理,沒有看到資料流,隻是看代碼的話,感覺暈頭轉向的,是以就想着補一些單元測試以使自己可以更好的了解項目代碼。問題是項目是跑在linux server上的,項目在開發的過程中沒有完全考慮支援跨平台,同時有些操作記憶體消耗也很大,是以并不是所有的代碼都可以在本地跑,等等,總之各種原因吧,我開始打算寫一個可以在linux下用跑測試代碼的工具。然後悲劇的發現除了會使用eclipse中的junit,其實我對junit一無所知。是以有些時候工具雖然能提供我們友善,但是它也隐藏了内部細節,最後我們自以為已經很了解某些東西了,其實離開了工具,我們一無所知。

最後加個注釋,這篇文章所有的代碼是基于junit4.10的,今天我發現這個版本和之前的版本(junit4.4)的代碼還是有比較大的差别的。本系列最後可能會涉及到一點和junit之前版本相關的資訊,不過這個就要看有沒有這個時間了。l

runner是junit的核心,它封裝了一個測試類中所有的測試方法,如blockjunit4classrunner,或者是多個runner,如suite;在運作過程中,周遊并運作所有測試方法(可以通過filter和sorter控制是否要執行某些測試方法以及執行測試方法的順序)。

在使用junit時,可能最常用的幾個注解就是@beforeclass、@afterclass、@before、@after、@test、@ignore了,這幾個注解所表達的意思相信很多人都很熟悉了,并且從它們的名字中也可以略知一二。這幾個注解,除了@test标記了哪個方法為測試方法,該方法必須為public,無傳回,不帶參;@ignore标明某個方法即使有@test的注解,也會忽略不運作,如junit文檔中解釋,在某些情況下,我們可能想臨時的不想執行某些測試方法,除了将該測試方法整個注釋掉,junit為我們提供了@ignore注解,此時即使某方法包含@test注解,該方法也不會作為測試方法執行,@ignore還可以注解在類上,當一個類存在@ignore注解時,該類所有的方法都不會被認為是測試方法;而剩下的四個注解則是junit為在測試類運作時的不同切面提供了切入點,如@beforeclass和@afterclass注解分别在測試類運作時前後各提供了一個切入點,這兩個注解必須使用在public,靜态,無傳回,不帶參的方法中,可以為每種注解指定多個方法,多個方法的執行順序依賴與java的反射機制,因而對一種注解的多個方法,在實際中不應該存在順序依賴,為一種注解寫多個方法的情況應該很少;而@before和@after注解則是在每個測試方法的運作前後各提供了一個切入點,這兩個注解必須使用在public,無傳回,不帶參的方法中。同@beforeclass和@afterclass,同一種注解可以注釋多個方法,他們的執行順序也依賴于反射機制,因而不能對順序有依賴。更直覺的,如下圖所示。

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

加點不完全相關的,這事實上是一種aop思想的實作。自從知道aop後,一直很喜歡這個想法,它事實上也是一種分層的思想。一個request從一個管道流進,經過層層處理後從另一個管道流出,在管道的流動過程中,每一層都對自己實作的功能做一些處理,如安全驗證、運作時間記錄、進入管道後打開某個連結出去之前關閉該連結、異常記錄等等相對獨立的功能都可以抽取出來到一個層中,進而在實際編碼業務邏輯過程中可以專注于業務,而不用管這些不怎麼相關但有必須有的邏輯,不僅是代碼的子產品分層更加清晰,減輕程式員的負擔,還提高了程式的安全性,因為這樣就可以部分避免有些事情必須要做容易忘了尴尬。aop的思想最出名的應該是spring中提供的支援了,但是我個人更喜歡struts2通過interceptor提供的aop實作,這是題外話。

為了更加清晰的了解junit的一些行為,我們先來看一下如下的一個測試例子:

 1 public class corejunit4sampletest {

 2     @beforeclass

 3     public static void beforeclass() {

 4         system.out.println("beforeclass() method executed.");

 5         system.out.println();

 6     }

 7     @beforeclass

 8     public static void beforeclass2() {

 9         system.out.println("beforeclass2() method executed.");

10         system.out.println();

11     }

12     @afterclass

13     public static void afterclass() {

14         system.out.println("afterclass() method executed.");

15         system.out.println();

16     }

17     @before

18     public void before() {

19         system.out.println("before() method executed.");

20     }

21     @after

22     public void after() {

23         system.out.println("after() method executed");

24     }

25     @test

26     public void testsucceeded() {

27         system.out.println("testsucceeded() method executed.");

28     }

29     @test

30     @ignore

31     public void testignore() {

32         system.out.println("testignore() method executed.");

33     }

34     @test

35     public void testfailed() {

36         system.out.println("testfailed() method executed.");

37         throw new runtimeexception("throw delibrately

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

");

38     }

39     @test

40     public void testassumptionfailed() {

41         system.out.println("testassumptionfailed() method executed.");

42         assume.assumethat(0, is.is(1));

43     }

44     @test

45     public void testfilteredout() {

46         system.out.println("testfilteredout() method executed.");

47     }

48 }

這個是一個簡單的測試類,内部實作基本上隻是列印,以确定測試方法的運作位置。該測試方法包括兩個@beforeclass注解的方法,以測試多個@beforeclass注解時他們的運作順序問題;@afterclass、@before、@after注解的方法各一個,以測試他們的運作位置問題;在多個@test注解的測試方法中,testsucceeded()測試方法用于測試通過時runlinstener的運作結果,testignore()測試方法測試@ignore注解對測試結果的影響,testfailed()方法測試在測試方法抛異常時runlistener的運作結果,testassumptionfailed()方法測試在測試方法斷言出錯時runlistener的運作結果,testfilteredout()方法測試filter的功能。在junit中,blockjunitclassrunner是其最核心的runner,它對一個隻包含測試方法的測試類的運作做了封裝,并且它還實作了filterable和sortable的接口,因而支援filter和sorter,為了更全面的展現junit提供的功能,我在這個例子中還加入了filter和sorter的測試,其中實作了一個filter類:methodnamefilter,在構造時指定要過濾掉的方法名,runner在運作之前調用filter()方法,以過濾掉這些方法。對于sorter隻實作了一個按字母序排列的comparator,它會以參數形式傳遞給sorter構造函數,以決定測試方法的順序,runner在運作之前調用sort()方法,以按指定的順序排列測試方法:

 1 public class methodnamefilter extends filter {

 2     private final set<string> excludedmethods = new hashset<string>();

 3     public methodnamefilter(string

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

 excludedmethods) {

 4         for(string method : excludedmethods) {

 5             this.excludedmethods.add(method);

 6         }

 7     }

 8     @override

 9     public boolean shouldrun(description description) {

10         string methodname = description.getmethodname();

11         if(excludedmethods.contains(methodname)) {

12             return false;

13         }

14         return true;

15     }

16     @override

17     public string describe() {

18         return this.getclass().getsimplename() + "-excluded methods: " + 

19                 excludedmethods;

21 }

22 public class alphabetcomparator implements comparator<description> {

23     @override

24     public int compare(description desc1, description desc2) {

25         return desc1.getmethodname().compareto(desc2.getmethodname());

26     }

27 }

由于本文主要講解junit中的runner的實作,因而在這個例子中,我将直接構造blockjunit4classrunner執行個體,以運作上述的測試類:

 1 public class blockjunit4classrunnerexecutor {

 2     public static void main(string[] args) {

 3         runnotifier notifier = new runnotifier();            

 4         result result = new result();

 5         notifier.addfirstlistener(result.createlistener());

 6         notifier.addlistener(new logrunlistener());

 7         

 8         runner runner = null;

 9         try {

10             runner = new blockjunit4classrunner(corejunit4sampletest.class);

11             try {

12                 ((blockjunit4classrunner)runner).filter(new methodnamefilter("testfilteredout"));

13             } catch (notestsremainexception e) {

14                 system.out.println("all methods are been filtered out");

15                 return;

16             }

17             ((blockjunit4classrunner)runner).sort(new sorter(new alphabetcomparator()));

18         } catch (throwable e) {

19             runner = new errorreportingrunner(corejunit4sampletest.class, e);

20         }

21         notifier.firetestrunstarted(runner.getdescription());

22         runner.run(notifier);

23         notifier.firetestrunfinished(result);

25 } 

junit會在runner運作之前通過runnotifier釋出testrunstarted事件表示junit運作開始,并在runner運作結束之後通過runnotifier釋出testrunfinished時間,表示junit運作結束。在runner運作過程中,在每個測試方法開始前也會通過runnotifier釋出teststarted事件,在測試方法結束後釋出testfinished事件(不管該測試方法通過還是未通過),若測試失敗,則釋出testfailure事件,若測試方法因調用assume類中的方法失敗(這種失敗不認為是測試失敗),則會釋出testassumptionfailure事件,若遇到一個ignore測試方法,釋出testignored事件。我們可以再runnotifier中加入要注冊的listener(事件接收器),如上例所示,為了測試,這個例子編寫的logrunlistener代碼如下:

 1 public class logrunlistener extends runlistener {

 2     public void testrunstarted(description description) throws exception {

 4         println("==>junit4 started with description: \n" + description);

 5         println();

 7     public void testrunfinished(result result) throws exception {

 8         println("==>junit4 finished with result: \n" + describe(result));

 9     }

10     public void teststarted(description description) throws exception{

11         println("==>test method started with description: " + description);

13     }

14     public void testfinished(description description) throws exception {

16         println("==>test method finished with description: " + description);

18         println();

19     }

20     public void testfailure(failure failure) throws exception {

21         println("==>test method failed with failure: " + failure);

22     }

23     public void testassumptionfailure(failure failure) {

24         println("==>test method assumption failed with failure: " + failure);

27     public void testignored(description description) throws exception {

28         println("==>test method ignored with description: " + description);

30         println();

31     }

32     private string describe(result result) {

33         stringbuilder builder = new stringbuilder();

34         builder.append("\tfailurecount: " + result.getfailurecount())

35                .append("\n");

36         builder.append("\tignorecount: " + result.getignorecount())

37                .append("\n");

38         builder.append("\truncount: " + result.getruncount())

39                .append("\n");;

40         builder.append("\truntime: " + result.getruntime())

41                .append("\n");

42         builder.append("\tfailures: " + result.getfailures())

43                .append("\n");;

44         return builder.tostring();

45     }    

46     private void println() {

47         system.out.println();

48     }

49     private void println(string content) {

50         system.out.println(content);

51     }

52 }

最後這個例子的運作結果(從上面的分析中,這個運作結果應該已經很清晰了,隻是有兩點需要注意,其一,@ignore注解的方法會被忽略不執行,包括@before、@after注解的方法,也不會觸發該teststarted事件,但是在result會記錄被忽略的測試方法數,而被filter過濾掉的方法(testfilteredout())則不會有任何記錄;其二,事件的觸發都是在@before注解之前或@after注解之後,事實上,如果測試方法中包含rule字段的話,也會在rule執行之前或之後,這就是junit抽象出的statement提供的特性,這是一個非常好的設計,這個特性将會在下一節:深入junit源碼之statement中講解):

==>junit4 started with description: 

levin.blog.junit.sample.simple.corejunit4sampletest

beforeclass2() method executed.

beforeclass() method executed.

==>test method started with description: testassumptionfailed(levin.blog.junit.sample.simple.corejunit4sampletest)

before() method executed.

testassumptionfailed() method executed.

after() method executed

==>test method assumption failed with failure: testassumptionfailed(levin.blog.junit.sample.simple.corejunit4sampletest): got: <0>, expected: is <1>

==>test method finished with description: testassumptionfailed(levin.blog.junit.sample.simple.corejunit4sampletest)

==>test method started with description: testfailed(levin.blog.junit.sample.simple.corejunit4sampletest)

testfailed() method executed.

==>test method failed with failure: testfailed(levin.blog.junit.sample.simple.corejunit4sampletest): throw delibrately

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

==>test method finished with description: testfailed(levin.blog.junit.sample.simple.corejunit4sampletest)

==>test method ignored with description: testignore(levin.blog.junit.sample.simple.corejunit4sampletest)

==>test method started with description: testsucceeded(levin.blog.junit.sample.simple.corejunit4sampletest)

testsucceeded() method executed.

==>test method finished with description: testsucceeded(levin.blog.junit.sample.simple.corejunit4sampletest)

afterclass() method executed.

==>junit4 finished with result: 

    failurecount: 1

    ignorecount: 1

    runcount: 3

    runtime: 36

    failures: [testfailed(levin.blog.junit.sample.simple.corejunit4sampletest): throw delibrately

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

]

從上面這個例子中,我們已經知道junit的核心功能以及不同注解方法執行的順序問題,然而既然本文是關注内部源碼的,因而接下來就要讨論如何實作上述的這些功能。

所謂面向對象中的類即是對某些事物或行為進行抽象和封裝,進而實作某些功能,一個類可以看成是一個子產品,它一般包含資料和行為,并提供給外界一定的接口,多個類之間通過各自的接口與外界互動,進而形成一個大的系統,有點類似分治的算法。在分治算法中最重要的是找到正确的方法以将問題劃分成各個區間,并最後使用一定的規則将各個區間解決的問題連結在一起,以解決整個系統的問題。在面向對象中同樣要找到一個正确的方法将系統的問題劃分成各個小子產品,用類來封裝,并定義類的接口以使各個類之間可以互動連結以形成一個大的系統。在實作中,我們也經常遇到某些事物是不同的,但是他們有一些共同的屬性或行為,在面向對象中對這個情況的處理是将相同的屬性或行為抽象成一個父類,而由各自的子類繼承父類提供各自不同的屬性和行為。然而有些時候,某些事物他們都具有某種行為,但是這些行為的結果卻是不同的,對這種情況,面向對象則采用多态的方式支援這種需求,即在父類中定義行為,在子類中重寫行為。在面向對象設計中,如何設計系統的類結構,包括類的繼承結構、類之間的互動接口等問題了是面向對象設計的核心問題。

junit将測試類中的方法(測試方法以及切面方法,@beforeclass、@afterclass、@before、@after等注解的方法)抽象成frameworkmethod類子產品,和測試相關的字段(由@rule和@classrule注解的字段)抽象成frameworkfield類子產品,而這兩個類具有一些共同的行為,如擷取在其之上的所有注解類、是否被其他相關成員隐藏等,因而junit将這些共同行為提取到父類frameworkmember中。對每個測試類,junit使用testclass類來封裝,testclass類以測試類的class執行個體為構造函數的參數,它收集測試類中所有junit識别的注解方法(@beforeclass、@afterclass、@before、@after、@test、@ignore)和注解字段(@rule、@classrule),以供其他類查詢。在每一次運作中,junit使用runner對其封裝,它可以是隻包含一個測試類的blockjunit4classrunner,也可以是包含多個runner的suite(這有點類似composite設計模式,以測試方法為葉子節點,以runner為包含葉子節點的節點将依次junit運作過程中的所有測試方法組成一棵樹);這兩個runner都繼承自parentrunner。parentrunner表達它是以一個在樹中具有子節點的節點,實作了filterable接口和sortable接口,以實作filter和sort的功能,parentrunner繼承自runner。runner是一個更高層次的抽象,目前在junit4中表達該runner隻是在執行樹中的沒有子節點的runner節點,如errorreportingrunner、ignoredclassrunner等;在junit3中不是采用注解的方式取得測試方法,為了相容性,junit4中也提供了junit38classrunner類以完成相容性的工作。runner實作了discribable接口,以表明可以通過description來描述一個runner;description主要是對runner和測試方法的描述,有點類似tostring()的味道。runner的每一次執行過程,如切面方法的執行、測試方法的執行、rule字段的執行等,junit都将其封裝在statement類中,一個測試方法的執行可能存在多個statement,他們形成鍊結構,這是一個我個人非常喜歡的設計,就像servlet中的filter、struts2的interceptor的設計類似,也是對aop的主要實作,這個内容将在另一節中介紹。runner在每個測試方法執行過程中都會通過runnotifier類釋出一些事件,如測試方法執行開始、結束、出錯、忽略等事件,runnotifier是runner對所有事件處理的封裝,我們可以通過它注冊runlistener的事件響應類,如上例的logrunlistener的注冊。到這裡,我們基本上已經介紹完了junit的所有的核心類簡單的功能和一些簡單的互動,那麼我們來看一下他們的類結構圖吧:

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

 description類和discribable接口的實作

看完類結構圖,那麼我們再來看一下源碼吧。由于description在junit中應用廣泛,又是相對獨立于功能的,因而将從description開始:

如上文所說,description是junit中對runner和測試方法的描述,它包含三個字段:

1 private final arraylist<description> fchildren;

2 private final string fdisplayname;    

3 private final annotation[] fannotations;

 displayname對測試方法的格式為:<methodname>(<classname>),如:

testfailed(levin.blog.junit.sample.simple.corejunit4sampletest)

對runner來說,displayname一般為runner所封裝的測試類,然而對沒有根類的suite,該值為”null”。annotations字段為測試方法或測試類上所具有的所有注解類。children對測試方法來說為空,對runner來說,表達runner内部所有的測試方法的description或runner的description。作為junit的使用者,除非自定義runner,其他的,我們一般都是通過注冊自己的runlistener來實作自己想要的統計和資訊提示工作,而在listener中并沒有直接暴露給我們runner或者是測試類的執行個體,它是通過提供description執行個體的方式來擷取我們需要的資訊。description提供以下的接口供我們使用:

 1 public string getdisplayname();

 2 public arraylist<description> getchildren();

 3 public boolean issuite();

 4 public boolean istest();

 5 public int testcount();

 6 public <t extends annotation> t getannotation(class<t> annotationtype);

 7 public collection<annotation> getannotations();

 8 public class<?> gettestclass();

 9 public string getclassname();

10 public string getmethodname();

runner實作了disacribable接口,該接口隻包含一個方法:

1 public interface describable {

2     public abstract description getdescription();

3 }

該方法在parentrunner中建立一個description,并周遊目前runner下的children,并将這些child的description添加到description中最後傳回:

 1 @override

 2 public description getdescription() {

 3     description description= description.createsuitedescription(

 4 getname(),    getrunnerannotations());

 5     for (t child : getfilteredchildren())

 6         description.addchild(describechild(child));

 7     return description;

 8 }

 9 blockjunit4classrunner:

10 @override

11 protected description describechild(frameworkmethod method) {

12     return description.createtestdescription(

13 gettestclass().getjavaclass(),

14                 testname(method), method.getannotations());

15 }

16 suite:

17 @override

18 protected description describechild(runner child) {

19     return child.getdescription();

20 }

21 

runlistener是junit提供的自定義對測試運作方法統計的接口,junit使用者可以繼承runlistener類,在junit運作開始、結束以及每一個測試方法的運作開始、結束、測試失敗以及假設出錯等情況下加入一些自己的邏輯,如統計整個junit運作的時間(這個在result中已經實作了)、每個運作方法的時間、運作最後有多少方法成功,多少失敗等,如上例中的logrunlistener。runlistener定義了如下接口:

 1 public class runlistener {

 4     }

 5     public void testrunfinished(result result) throws exception {

 7     public void teststarted(description description) throws exception {

 8     }

 9     public void testfinished(description description) throws exception {

12     public void testfailure(failure failure) throws exception {

14     public void testassumptionfailure(failure failure) {

16     public void testignored(description description) throws exception {

17     }

18 }

這個類需要注意的是:1. testfinished()不管測試方法是成功還是失敗,這個方法總是會被調用;2. 在測試方法中跑出assumptionviolatedexception并不認為是測試失敗,一般在測試方法中調用assume類中的方法而失敗,會跑該異常,在junit的預設實作中,對這些方法隻是簡單的忽略,并釋出testassumptionfailure()事件,并不認為該方法測試失敗,自定義的runner可以改變這個行為;3. 當我們需要自己操作runner執行個體是,result的資訊需要自己手動的注冊result中定義的listener,不然result中的資訊并不會填寫正确,如上例中的做法:

1 result result = new result();

2 notifier.addfirstlistener(result.createlistener());

在實作時,所有事件響應函數提供給我們有三種資訊:description、result和failure。其中description已經在上一小節中介紹過了它所具有的資訊,這裡不再重複。對于result,前段提到過隻有它提供的listener後才會取到正确的資訊,它包含的資訊有:總共執行的測試方法數、忽略的測試方法數、以及所有在測試過程中抛出的異常清單、整個測試過程的執行時間等,result執行個體在junit運作結束時傳入testrunfinished()事件方法中,以幫助我們做一些測試方法執行結果的統計資訊,事實上,我感覺這些資訊很多時候還是不夠的,需要我們在自己的runlistener中自己做一些資訊記錄。failure類則記錄了測試方法在測試失敗時抛出的異常以及該測試方法對應的description執行個體,當在建構runner過程中出現異常,failure也會用于描述測試類,如上例中,如果beforeclass()方法不是靜态的話,在初始化runner時就會出錯,此時的運作結果如下:

==>test method started with description: initializationerror(levin.blog.junit.sample.simple.corejunit4sampletest)

==>test method failed with failure: initializationerror(levin.blog.junit.sample.simple.corejunit4sampletest): method beforeclass() should be static

==>test method finished with description: initializationerror(levin.blog.junit.sample.simple.corejunit4sampletest)

    ignorecount: 0

    runcount: 1

    runtime: 3

    failures: [initializationerror(levin.blog.junit.sample.simple.corejunit4sampletest): method beforeclass() should be static]

runner中的run()方法需要傳入runnotifier執行個體,它是對runlistener的封裝,我們可以向其注冊多個runlistener,當runner需要釋出某個事件時,就會通過它來代理,它則會周遊所有已注冊的runlistener,并運作相應的事件。當運作某個runlistener抛異常時,它會首先将這個runlistener移除,并釋出測試失敗事件,在該事件中的description為一個名為“test mechanism”的description,因為此時是注冊的事件處理器失敗,無法獲知一個description執行個體:

 1 private abstract class safenotifier {

 2     void run() {

 3         synchronized (flisteners) {

 4             for (iterator<runlistener> all= flisteners.iterator(); 

 5                             all.hasnext();)

 6                 try {

 7                     notifylistener(all.next());

 8                 } catch (exception e) {

 9                     all.remove(); // remove the offending listener first to avoid an infinite loop

11                     firetestfailure(new failure(

12                             description.test_mechanism, e));

13                 }

14         }

16     abstract protected void notifylistener(runlistener each) throws exception;

在runnotifier類中還有一個pleasestop()方法進而可以在測試中途停止整個測試過程,其實作時在釋出teststarted事件時,如果發現pleasestop字段已經為true,則抛出stoppedbyuserexception,當runner接收到該異常後,将該異常直接抛出。

事實上,parentrunner在釋出事件時,并不直接和runnotifier打交道,而是會用eachtestnotifier類對其進行封裝,該類隻是對multiplefailureexception做了處理,其他隻是一個簡單的代理。在遇到multiplefailureexception,它會周遊内部每個exception,并對每個exception釋出testfailure事件。當在運作@after注解的方法時抛出多個異常類(比如測試方法已經抛出異常了,@after注解方法中又抛出異常或者存在多個@after注解方法,并多個@after注解方法抛出了多個異常),此時就會構造一個multiplefailureexception,這種設計可能是出于對防止測試方法中的exception被@after注解方法中的exception覆寫的問題引入的。

frameworkmethod是對java反射中method的封裝,它提供了對方法的驗證、調用以及處理子類方法隐藏父類方法問題,其主要提供的接口如下:

 1 public method getmethod();

 2 public object invokeexplosively(final object target, final object

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

 params);

 3 public string getname();

 4 public void validatepublicvoidnoarg(boolean isstatic, list<throwable> errors);

 5 public void validatepublicvoid(boolean isstatic, list<throwable> errors) 

 6 public void validatenotypeparametersonargs(list<throwable> errors);

 7 @override

 8 public boolean isshadowedby(frameworkmethod other);

 9 @override

10 public annotation[] getannotations();

11 public <t extends annotation> t getannotation(class<t> annotationtype);

其中isshadowedby()方法用于處理子類方法隐藏父類方法的問題,junit支援測試類存在繼承關系,并且會周遊所有父類的測試方法,但是如果子類的方法隐藏了父類的方法,則父類的方法不會被執行。這裡的隐藏是指當子類的方法名、所有參數類型相同時,不管是靜态方法還是非靜态方法,都會被隐藏。其實作如下:

 1 public boolean isshadowedby(frameworkmethod other) {

 2     if (!other.getname().equals(getname()))

 3         return false;

 4     if (other.getparametertypes().length != getparametertypes().length)

 5         return false;

 6     for (int i= 0; i < other.getparametertypes().length; i++)

 7         if (!other.getparametertypes()[i].equals(getparametertypes()[i]))

 9             return false;

10     return true;

11 }

類似frameworkmethod,frameworkfield是對java反射中的field的封裝,它提供了對字段取值、處理隐藏父類字段的問題,其主要提供的接口如下:

 1 public string getname();

 2 @override

 3 public annotation[] getannotations();

 4 public boolean ispublic();

 5 @override

 6 public boolean isshadowedby(frameworkfield othermember) {

 7     return othermember.getname().equals(getname());

 9 public boolean isstatic();

10 public field getfield();

11 public class<?> gettype();

12 public object get(object target) 

13 throws illegalargumentexception, illegalaccessexception;

在處理隐藏問題是,它隻是判斷如果父類中某個字段的名和子類中某個字段的名字相同,則父類中的字段不會被junit處理,即被隐藏。事實上,關于隐藏的問題,隻是對junit識别的方法和字段有效,即有junit相關注解的方法字段,其他方法并不受影響(事實上也是不可能受到影響)。

testclass是對java中class類的封裝,在testclass構造過程中,它會收集所有傳入class執行個體中具有注釋的字段和方法,它會搜尋類的所有繼承結構。因而testclass的成員如下:

1 private final class<?> fclass;

2 private map<class<?>, list<frameworkmethod>> fmethodsforannotations;

3 private map<class<?>, list<frameworkfield>> ffieldsforannotations;

并且這些成員在testclass構造完後即已經初始化完成。在所有注解的收集過程中,它對@before和@beforeclass有一個特殊的處理,即父類中@before和@beforeclass的注解方法總是在之類之前,進而保證父類的@before和@beforeclass的注解方法會在子類的這些注解方法之前執行。而對@after和@afterclass的注解方法沒有做處理,因而保持了之類的@after和@afterclass注解方法會在父類的這些注解方法之前執行。這段特殊邏輯是通過以下代碼實作的(在addtoannotationlists()方法中,其中runstoptobottom()方法形象的表達了這個意思):

1 if (runstoptobottom(type))

2     members.add(0, member);

3 else

4     members.add(member);

在獲得測試類中所有注解的方法和字段的map後,testclass提供了對annotation對應的方法和字段的查詢接口:

1 public list<frameworkmethod> getannotatedmethods(

2             class<? extends annotation> annotationclass);

3 public list<frameworkfield> getannotatedfields(

4             class<? extends annotation> annotationclass);

5 public <t> list<t> getannotatedfieldvalues(object test,

6             class<? extends annotation> annotationclass, 

7 class<t> valueclass);

以及測試類相關的資訊,如class執行個體、名字、測試類中具有的annotation等:

1 public class<?> getjavaclass();

2 public string getname();

3 public constructor<?> getonlyconstructor();

4 public annotation[] getannotations();

5 public boolean isanonstaticinnerclass();

runner是junit中對所有runner的抽象,它隻包括三個方法:

1 public abstract class runner implements describable {

3     public abstract void run(runnotifier notifier);

4     public int testcount() {

5         return getdescription().testcount();

6     }

7 }

 其中getdescription()的實作已經在description相關的小節中做過計算了,不在重複,testcount()方法很簡單,也已經實作了,因而本節主要介紹run()方法的運作過程,而該方法的實作則是在parentrunner中。

用parentrunner命名這個runner是想表達這是一個在測試方法樹中具有子節點的節點,它的子節點可以是測試方法(blockjunit4classrunner)或者runner(suite)。parentrunner以測試類class執行個體作為構造函數的參數,在構造函數内部用testclass對測試類進行封裝,并對一些那些規定的方法做驗證:

1.       @beforeclass和@afterclass注解的方法必須是public,void,static,無參

1 validatepublicvoidnoargmethods(beforeclass.class, true, errors);

2 validatepublicvoidnoargmethods(afterclass.class, true, errors);

 2.       對有@classrule修飾的字段,必須是public,static,并且該字段的執行個體必須是實作了testrule接口的。為了相容性,也可以實作methodrule接口。

在驗證過程中,parentrunner通過一個list<throwable>收集所有的驗證錯誤資訊,如果存在錯誤資訊,則驗證不通過,parentrunner将抛出一個initializationerror的exception,該exception中包含了所有的錯誤資訊,一般在這種情況下,會建立一個errorreportingrunner傳回以處理驗證出錯的問題,該runner将在下面的小節中詳細介紹。

在runner構造完成後,就可以調用run()方法運作該runner了:

 2 public void run(final runnotifier notifier) {

 3     eachtestnotifier testnotifier= new eachtestnotifier(notifier, getdescription());

 5     try {

 6         statement statement= classblock(notifier);

 7         statement.evaluate();

 8     } catch (assumptionviolatedexception e) {

 9         testnotifier.firetestignored();

10     } catch (stoppedbyuserexception e) {

11         throw e;

12     } catch (throwable e) {

13         testnotifier.addfailure(e);

14     }

該方法的核心是構造一個statement執行個體,然後調用該執行個體的evaluate()方法。在junit中,statement是一個類連結清單,一個runner的執行過程就是這個statement類連結清單的執行過程。parentrunner對statement類連結清單的構造主要是對測試類級别的構造,如@beforeclass、@afterclass、@classrule等執行statement:

 1 protected statement classblock(final runnotifier notifier) {

 2     statement statement= childreninvoker(notifier);

 3     statement= withbeforeclasses(statement);

 4     statement= withafterclasses(statement);

 5     statement= withclassrules(statement);

 6     return statement;

 7 }

 8 protected statement childreninvoker(final runnotifier notifier) {

 9     return new statement() {

10         @override

11         public void evaluate() {

12             runchildren(notifier);

14     };

16 private void runchildren(final runnotifier notifier) {

17     for (final t each : getfilteredchildren())

18         fscheduler.schedule(new runnable() {

19             public void run() {

20                 parentrunner.this.runchild(each, notifier);

21             }

22         });

23     fscheduler.finished();

24 }

25 private list<t> getfilteredchildren() {

26     if (ffilteredchildren == null)

27         ffilteredchildren = new arraylist<t>(getchildren());

28     return ffilteredchildren;

29 }

30 protected abstract list<t> getchildren();

31 protected abstract void runchild(t child, runnotifier notifier);

其中getchildren()、runchild()方法由子類實作。

 1 protected statement withbeforeclasses(statement statement) {

 2     list<frameworkmethod> befores= ftestclass

 3             .getannotatedmethods(beforeclass.class);

 4     return befores.isempty() ? statement :

 5         new runbefores(statement, befores, null);

 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 private statement withclassrules(statement statement) {

14     list<testrule> classrules= classrules();

15     return classrules.isempty() ? statement :

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

17 }

18 protected list<testrule> classrules() {

19     return ftestclass.getannotatedfieldvalues(null, 

20         classrule.class, testrule.class);

關于statement将在下一節中詳細講,不過這裡從statement的構造過程可以看出rule的執行要先于@beforeclass注解方法或晚于@afterclass注解方法。

在parentrunner關于執行方法的實作中,還有一個對每個測試方法statement鍊的執行架構的實作,其主要功能是加入事件釋出和對異常的處理邏輯,從這裡的statement執行個體是一個測試方法的運作整體,它包括了@before注解方法、@after注解方法以及@rule注解字段的testrule執行個體的運作過程,因而teststarted事件的釋出是在@before方法運作之前,而testfinished事件的釋出是在@after方法運作之後:

 1 protected final void runleaf(statement statement, description description,    runnotifier notifier) {

 3     eachtestnotifier eachnotifier= new eachtestnotifier(notifier, description);

 5     eachnotifier.fireteststarted();

 6     try {

 9         eachnotifier.addfailedassumption(e);

10     } catch (throwable e) {

11         eachnotifier.addfailure(e);

12     } finally {

13         eachnotifier.firetestfinished();

parentrunner還實作filterable接口和sortable接口,不過這裡很奇怪的實作時為什麼sorter要儲存成字段,感覺這個完全可以通過方法參數實作,而filteredchildren字段的實作方式在多線程環境中運作的話貌似也會出問題。filter類主要提供一個shouldrun()接口以判斷傳入的description是否可以運作,若否,則将該description從parentrunner中移除;否則,将該filter應用到該child中,以處理child可能是filterable執行個體(parentrunner)的問題,進而以遞歸、先根周遊的方式周遊測試執行個體樹上的所有節點,若一個父節點的所有子節點都被過濾了,則抛出notestremainexception:

 1 public void filter(filter filter) throws notestsremainexception {

 2     for (iterator<t> iter = getfilteredchildren().iterator(); 

 3                         iter.hasnext(); ) {

 4         t each = iter.next();

 5         if (shouldrun(filter, each))

 6             try {

 7                 filter.apply(each);

 8             } catch (notestsremainexception e) {

 9                 iter.remove();

10             }

11         else

12             iter.remove();

14     if (getfilteredchildren().isempty()) {

15         throw new notestsremainexception();

18 private boolean shouldrun(filter filter, t each) {

19     return filter.shouldrun(describechild(each));

junit中提供幾種預設實作的filter:1. all不做任何過濾;2. matchmethoddescription隻保留某個指定的測試方法。

 1 public static filter all= new filter() {

 2     @override

 3     public boolean shouldrun(description description) {

 4         return true;

 5     }

 6     @override

 7     public string describe() {

 8         return "all tests";

10     @override

11     public void apply(object child) throws notestsremainexception {

12         // 因為不會做任何過濾行為,因而不需要應用到子節點中

14     @override

15     public filter intersect(filter second) {

16         return second; // 因為本身沒有任何過濾行為,是以可以直接傳回傳入的filter

18 };

19 public static filter matchmethoddescription(final description desireddescription) {

21     return new filter() {

22         @override

23         public boolean shouldrun(description description) {

24             if (description.istest())

25                 return desireddescription.equals(description);

26             // explicitly check if any children want to run

27             for (description each : description.getchildren())

28                 if (shouldrun(each))

29                     return true;

30             return false;                    

31         }

32         @override

33         public string describe() {

34             return string.format("method %s", desireddescription.getdisplayname());

36         }

37     };

38 }

sorter類實作comparator接口,parentrunner通過其compare()方法構造和parentrunner子節點相關的comparator執行個體,并采用後根遞歸周遊的方式對測試方法樹中的所有測試方法進行排序,因而這裡隻會排序同一個節點下的所有子節點,而節點之間的順序不會受影響。

 1 public void sort(sorter sorter) {

 2     fsorter= sorter;

 3     for (t each : getfilteredchildren())

 4         sortchild(each);

 5     collections.sort(getfilteredchildren(), comparator());

 7 private comparator<? super t> comparator() {

 8     return new comparator<t>() {

 9         public int compare(t o1, t o2) {

10             return fsorter.compare(describechild(o1), describechild(o2));

12         }

13     };

14 }

最後parentrunner還提供了一個runnerscheduler的接口字段,以控制junit測試方法的執行過程,我們可以注入一個使用多線程方式運作每個測試方法的runnerscheduler,不過從代碼上看,貌似junit對多線程的支援并不好,是以這個接口的擴充目前來看我還找不到什麼用途:

1 public interface runnerscheduler {

2     void schedule(runnable childstatement);

3     void finished();

4 }

blockjunit4classrunner節點下所有的子節點都是測試方法,它是junit中運作測試方法的核心runner,它實作了parentrunner中沒有實作的幾個方法:

  1 @override

  2 protected list<frameworkmethod> getchildren() {

  3     return computetestmethods();

  4 }

  5 protected list<frameworkmethod> computetestmethods() {

  6     return gettestclass().getannotatedmethods(test.class);

  7 }

  8 @override

  9 protected void runchild(final frameworkmethod method, runnotifier notifier) {

 11     description description= describechild(method);

 12     if (method.getannotation(ignore.class) != null) {

 13         notifier.firetestignored(description);

 14     } else {

 15         runleaf(methodblock(method), description, notifier);

 16     }

 17 }

 18 protected statement methodblock(frameworkmethod method) {

 19     object test;

 20     try {

 21         test= new reflectivecallable() {

 22             @override

 23             protected object runreflectivecall() throws throwable {

 24                 return createtest();

 25             }

 26         }.run();

 27     } catch (throwable e) {

 28         return new fail(e);

 29     }

 30     statement statement= methodinvoker(method, test);

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

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

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

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

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

 36     return statement;

 37 } //這個方法的實作可以看出每個測試方法運作時都會重新建立一個新的測試類執行個體,這也可能是@beforeclass、afterclass、@classrule需要靜态的原因吧,因為靜态的話,每次類執行個體的重新建立對其結果都不會有影響。

 38 另,從這裡對statement的建構順序,junit對testrule的運作也要在@before注解方法之前或@after注解方法之後

 39 protected object createtest() throws exception {

 40     return gettestclass().getonlyconstructor().newinstance();

 41 }

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

 43     return new invokemethod(method, test);

 44 }

 45 protected statement possiblyexpectingexceptions(frameworkmethod method, object test, statement next) {

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

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

 49             getexpectedexception(annotation)) : next;

 50 }

 51 private class<? extends throwable> getexpectedexception(test annotation) {

 52     if (annotation == null || annotation.expected() == none.class)

 53         return null;

 54     else

 55         return annotation.expected();

 56 }

 57 private boolean expectsexception(test annotation) {

 58     return getexpectedexception(annotation) != null;

 59 }

 60 protected statement withpotentialtimeout(frameworkmethod method,

 61         object test, statement next) {

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

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

 64 }

 65 private long gettimeout(test annotation) {

 66     if (annotation == null)

 67         return 0;

 68     return annotation.timeout();

 69 }

 70 protected statement withbefores(frameworkmethod method, object target,

 71         statement statement) {

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

 73             before.class);

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

 75             befores, target);

 76 }

 77 protected statement withafters(frameworkmethod method, object target,

 78         statement statement) {

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

 80             after.class);

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

 82             afters, target);

 83 }

 84 private statement withrules(frameworkmethod method, object target,

 85         statement statement) {

 86     statement result= statement;

 87     result= withmethodrules(method, target, result);

 88     result= withtestrules(method, target, result);

 89     return result;

 90 }

 91 private statement withmethodrules(frameworkmethod method, object target,

 92         statement result) {

 93     list<testrule> testrules= gettestrules(target);

 94     for (org.junit.rules.methodrule each : getmethodrules(target))

 95         if (! testrules.contains(each))

 96             result= each.apply(result, method, target);

 97     return result;

 98 }

 99 private list<org.junit.rules.methodrule> getmethodrules(object target) {

100     return rules(target);

101 }

102 protected list<org.junit.rules.methodrule> rules(object target) {

103     return gettestclass().getannotatedfieldvalues(target, rule.class,

104             org.junit.rules.methodrule.class);

105 }

106 private statement withtestrules(frameworkmethod method, object target,

107         statement statement) {

108     list<testrule> testrules= gettestrules(target);

109     return testrules.isempty() ? statement :

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

111 }

112 protected list<testrule> gettestrules(object target) {

113     return gettestclass().getannotatedfieldvalues(target,

114             rule.class, testrule.class);

115 }

在驗證方面,blockjunit4classrunner也加入了一些和自己相關的驗證:  

1 @override

2 protected void collectinitializationerrors(list<throwable> errors) {

3     super.collectinitializationerrors(errors);

4     validatenononstaticinnerclass(errors);

5     validateconstructor(errors);

6     validateinstancemethods(errors);

7     validatefields(errors);

8 }

1.       如果測試類是一個類的内部類,那麼該測試類必須是靜态的:

1 protected void validatenononstaticinnerclass(list<throwable> errors) {

2     if (gettestclass().isanonstaticinnerclass()) {

3         string gripe= "the inner class " + gettestclass().getname()

4                 + " is not static.";

5         errors.add(new exception(gripe));

2.       測試類的構造函數必須有且僅有一個無參的構造函數(事實上關于隻有一個構造函數的驗證在構造testclass執行個體的時候已經做了,因而這裡真正起作用的知識對無參的驗證):

 1 protected void validateconstructor(list<throwable> errors) {

 2     validateonlyoneconstructor(errors);

 3     validatezeroargconstructor(errors);

 4 }

 5 protected void validateonlyoneconstructor(list<throwable> errors) {

 6     if (!hasoneconstructor()) {

 7         string gripe= "test class should have exactly one public constructor";

 8         errors.add(new exception(gripe));

10 }

11 protected void validatezeroargconstructor(list<throwable> errors) {

12     if (!gettestclass().isanonstaticinnerclass()

13             && hasoneconstructor()

14             && (gettestclass().getonlyconstructor().getparametertypes().length != 0)) {

16         string gripe= "test class should have exactly one public zero-argument constructor";

17         errors.add(new exception(gripe));

18     }

19 }

20 private boolean hasoneconstructor() {

21     return gettestclass().getjavaclass().getconstructors().length == 1;

22 }

3.       @before、@after、@test注解的方法必須是public,void,非靜态,不帶參數:

 1 protected void validateinstancemethods(list<throwable> errors) {

 2     validatepublicvoidnoargmethods(after.class, false, errors);

 3     validatepublicvoidnoargmethods(before.class, false, errors);

 4     validatetestmethods(errors);

 5     if (computetestmethods().size() == 0)

 6         errors.add(new exception("no runnable methods"));

 8 protected void validatetestmethods(list<throwable> errors) {

 9     validatepublicvoidnoargmethods(test.class, false, errors);

4.       帶有@rule注解的字段必須是public,非靜态,實作了testrule接口或methodrule接口。

1 private void validatefields(list<throwable> errors) {

2     rule_validator.validate(gettestclass(), errors);

寫了好幾天,終于把junit核心的執行所有代碼分析完了,不過發現寫的那麼細,很多東西其實很難表達,因而貼了很多代碼,感覺寫的挺亂的,是以畫一張序列圖吧,感覺很多時候還是圖的表達效果更好一些,要我看那麼一大段的問題,也感覺挺煩的。

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

suite是其子節點是runner的runner,其内部儲存了一個runner的list。suite的功能實作很簡單,因為它将大部分的方法代理給了其内部runner執行個體:

 1 private final list<runner> frunners;

 3 protected list<runner> getchildren() {

 4     return frunners;

 5 }

 6 @override

 7 protected description describechild(runner child) {

 8     return child.getdescription();

 9 }

11 protected void runchild(runner runner, final runnotifier notifier) {

12     runner.run(notifier);

13 }

suite重點在于如何建構一個suite,suite提供兩個構造函數:

1.       提供一個帶suiteclasses注解的類,所有測試類由suiteclasses指定,而klass類作為這個suite的根類。此時一般所有的測試類是klass的内部類,因而可以通過@runwith指定運作klass類的runner是suite,然後用suiteclasses注解指定可以作為測試類的類。

 1 public suite(class<?> klass, runnerbuilder builder) throws initializationerror {

 3     this(builder, klass, getannotatedclasses(klass));

 5 protected suite(runnerbuilder builder, class<?> klass, class<?>[] suiteclasses) throws initializationerror {

 7     this(klass, builder.runners(klass, suiteclasses));

 9 @retention(retentionpolicy.runtime)

10 @target(elementtype.type)

11 @inherited

12 public @interface suiteclasses {

13     public class<?>[] value();

15 private static class<?>[] getannotatedclasses(class<?> klass) throws initializationerror {

17     suiteclasses annotation= klass.getannotation(suiteclasses.class);

18     if (annotation == null)

19         throw new initializationerror(string.format("class '%s' must have a suiteclasses annotation", klass.getname()));

22     return annotation.value();

23 }

 2.       在有些情況下,我們需要運作多個沒有相關的測試類,此時這些測試類沒有一個公共的根類的suite,則需要使用一下的構造函數(此時suite的getname()傳回”null”,即其description的displayname的值為”null”,關于runnerbuilder将會在後面的文章中介紹):

 1 public suite(runnerbuilder builder, class<?>[] classes) throws initializationerror {

 3     this(null, builder.runners(null, classes));

 5 protected suite(class<?> klass, list<runner> runners) throws initializationerror {

 7     super(klass);

 8     frunners = runners;

parameterized繼承自suite,它類似blockjunit4classrunner是對一個測試類的運作過程的封裝,但是它支援在測試類中定義一個獲得參數數組的一個清單,進而可以為構造該測試類提供不同的參數值以進行多次測試。因而parameterized這個runner其實就是根據參數數組清單的個數建立多個基于該測試類的runner,并運作所有這些runner中測試方法。

由于這個測試類的構造函數是帶參的,而blockjunit4classrunner則限制其測試類必須是不帶參的,因而parameterized需要建立自己的runner,它內建自blockjunit4classrunner,除了構造函數的差别,parameterized的根節點是該測試類本身,它處理@beforeclass、@afterclass、@classrule的注解問題,因而其子節點的runner不需要重新對其做處理了,因而在內建的runner中應該避免這些方法、字段重複執行。

 1 private class testclassrunnerforparameters extends blockjunit4classrunner {

 3     private final int fparametersetnumber;

 4     private final list<object[]> fparameterlist;

 5     testclassrunnerforparameters(class<?> type, list<object[]> parameterlist, int i) throws initializationerror {

 8         super(type);

 9         fparameterlist= parameterlist;

10         fparametersetnumber= i; //參數數組清單中的位置

12     @override

13     public object createtest() throws exception {

14         return gettestclass().getonlyconstructor().newinstance(

15                 computeparams());//帶參建立測試類執行個體

17     private object[] computeparams() throws exception {

18         try {

19             return fparameterlist.get(fparametersetnumber);

20         } catch (classcastexception e) {

21             throw new exception(string.format(

22                     "%s.%s() must return a collection of arrays.",

23                     gettestclass().getname(), getparametersmethod(

24                             gettestclass()).getname()));

25         }

27     @override

28     protected string getname() {

29         return string.format("[%s]", fparametersetnumber);

30     }

31     @override

32     protected string testname(final frameworkmethod method) {

33         return string.format("%s[%s]", method.getname(),

34                 fparametersetnumber);

35     }

36     @override

37     protected void validateconstructor(list<throwable> errors) {

38         validateonlyoneconstructor(errors);//去除不帶參構造函數驗證

39     }

40     @override

41     protected statement classblock(runnotifier notifier) {

42         return childreninvoker(notifier);//不處理@beforeclass、

43 //@afterclass、@classrule等注解

44     }

45     @override

46     protected annotation[] getrunnerannotations() {

47         return new annotation[0];//去除測試類的annotation,這些應該在parameterized中處理

49     }

50 }

parameterized對suite的行為改變隻是在建立runner集合的過程,parameterized通過類中查找@parameters注解的靜态方法獲得參數數組清單,并更具這個參數數組清單建立testclassrunnerforparameters的集合:

 1 @retention(retentionpolicy.runtime)

 2 @target(elementtype.method)

 3 public static @interface parameters {

 5 public parameterized(class<?> klass) throws throwable {

 6     super(klass, collections.<runner>emptylist());

 7     list<object[]> parameterslist= getparameterslist(gettestclass());

 8     for (int i= 0; i < parameterslist.size(); i++)

 9         runners.add(new testclassrunnerforparameters(gettestclass().getjavaclass(), parameterslist, i));

12 private list<object[]> getparameterslist(testclass klass)

13         throws throwable {

14     return (list<object[]>) getparametersmethod(klass).invokeexplosively(null);

16 }

17 private frameworkmethod getparametersmethod(testclass testclass)

18         throws exception {

19     list<frameworkmethod> methods= testclass.getannotatedmethods(parameters.class);

21     for (frameworkmethod each : methods) {

22         int modifiers= each.getmethod().getmodifiers();

23         if (modifier.isstatic(modifiers) && 

24                 modifier.ispublic(modifiers))

25             return each;

27     throw new exception("no public static parameters method on class " + testclass.getname());

一個簡單的parameterized測試類如下,不過如果在eclipse中單獨的運作testequal()測試方法會出錯,因為eclipse在filter傳入的方法名為testequal,而在parameterized中這個方法名已經被改寫成testequal[i]了,因而在filter過程中,所有的測試方法都被過濾掉了,此時會抛notestremainexception,而在eclipse中出現的則是initializationerror的exception,這裡也是eclipse中junit的插件沒有做好,它并沒有給出failure類的詳細資訊:

 1 public class parameterizedtest {

 2     @parameters

 3      public static list<object[]> data() {

 4          return arrays.aslist(new object[][] {

 5                  { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, 

 6                  { 4, 3 }, { 5, 5 }, { 6, 8 }

 7          });

 8      }

 9      @beforeclass

10      public static void beforeclass() {

11          system.out.println("

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

.testing

深入JUnit源碼之Runner寫在前面的話深入JUnit源碼之Runner

.");

12      }

13      private final int input;

14      private final int expected;

15      public parameterizedtest(int input, int expected) {

16          this.input = input;

17          this.expected = expected;

18      }

19      @test

20      public void testequal() {

21          assert.assertequals(expected, compute(input));

22      }

23      public int compute(int input) {

24          if(input == 0 || input == 1) {

25              return input;

26          }

27          if(input == 2) {

28              return 1;

29          }

30          return compute(input -1) + compute(input - 2);

31      }

32 }

關于junit4,還剩下最後兩個runner,分别是errorreportingrunner和ignoredclassrunner,為了程式設計模型的統一(這是一個非常好的設計想法,将各種變化都封裝在runner中),junit中即使一個runner執行個體建立失敗或是該測試類有@ignored的注解,在這兩種情況中,junit分别通過errorreportingrunner和ignoredclassrunner去表達。在errorreportingrunner中,為每個exception釋出測試失敗的資訊;ignoredclassrunner則隻是釋出testignored事件:

 1 public class errorreportingrunner extends runner {

 2     private final list<throwable> fcauses;

 3     private final class<?> ftestclass;

 4     public errorreportingrunner(class<?> testclass, throwable cause) {

 5         ftestclass= testclass;

 6         fcauses= getcauses(cause);

 9     public description getdescription() {

10         description description= description.createsuitedescription(ftestclass);

12         for (throwable each : fcauses)

13             description.addchild(describecause(each));

14         return description;

17     public void run(runnotifier notifier) {

18         for (throwable each : fcauses)

19             runcause(each, notifier);

21     @suppresswarnings("deprecation")

22     private list<throwable> getcauses(throwable cause) {

23         if (cause instanceof invocationtargetexception)

24             return getcauses(cause.getcause());

25         if (cause instanceof initializationerror)

26             return ((initializationerror) cause).getcauses();

27         if (cause instanceof org.junit.internal.runners.initializationerror)

28             return ((org.junit.internal.runners.initializationerror) cause)

29                     .getcauses();

30         return arrays.aslist(cause);

32     private description describecause(throwable child) {

33         return description.createtestdescription(ftestclass,

34                 "initializationerror");

36     private void runcause(throwable child, runnotifier notifier) {

37         description description= describecause(child);

38         notifier.fireteststarted(description);

39         notifier.firetestfailure(new failure(description, child));

40         notifier.firetestfinished(description);

41     }

42 }

43 public class ignoredclassrunner extends runner {

44     private final class<?> ftestclass;

45     public ignoredclassrunner(class<?> testclass) {

46         ftestclass= testclass;

48     @override

49     public void run(runnotifier notifier) {

50         notifier.firetestignored(getdescription());

52     @override

53     public description getdescription() {

54         return description.createsuitedescription(ftestclass);

55     }

56 }

寫了一個周末了,這一篇終于是寫完了,初次嘗試将閱讀架構源碼以文字的形式表達出來,确實不好寫,感覺自己寫的太詳細了,也貼了太多源碼,因為有些時候感覺源碼比文字更有表達能力,不過太多的源碼就把很多實質的東西掩蓋了,然後文章看起來也沒有什麼大的價值幹,而且太長了,看到那麼多的東西,要我自己看到也就有不想看的感覺,1萬多字啊,呵呵。總之以後慢慢改進吧,這一篇就當是練習了,不想複工了,也沒那麼多的時間重寫。。。。。。

繼續閱讀