初次用文字的方式記錄讀源碼的過程,不知道怎么寫,感覺有點貼代碼的嫌疑。不過中間還是加入了一些自己的理解和心得,希望以后能夠慢慢的改進,感興趣的童鞋湊合著看吧,感覺JUnit這個框架還是值得看的,里面有許多不錯的設計思想在,更何況它是Kent Beck和Erich Gamma這樣的大師寫的。。。。。
深入JUnit源碼之Builder、Request與JUnitCore
經(jīng)過前面三節(jié)的Runner、Statement、Rule的講解,事實上JUnit的核心運行邏輯已經(jīng)完成了,剩下的就是一些外圍的支持代碼,包括Runner的構(gòu)建、用Assert對測試方法的運行結(jié)果的驗證代碼以及為了兼容性而存在的一些代碼。本節(jié)將關(guān)注Runner的構(gòu)建部分,在JUnit中通過Request和RunnerBuilder共同支持。
在JUnit中,RunnerBuilder對根據(jù)測試類創(chuàng)建Runner邏輯的封裝,特別是它支持@RunWith注解以在測試類中指定需要的執(zhí)行的Runner,這也是自定義的Runner可以很方便的插入JUnit框架運行的原因,這個設計其實也蠻具有參考價值的:通過注解的方式為用戶提供插入點,以擴展框架本身的功能;而在實現(xiàn)工程中,通過外圍Builder來支持,從而不影響核心設計。
Request有點類似配置實例的感覺,用戶可以根據(jù)自己的需求來定制Runner創(chuàng)建的信息,如代理給RunnerBuilder為每個測試類創(chuàng)建相應的Runner,根據(jù)用戶的需求以決定是否要有filter和sort操作等。用戶可以定義自己的Request實例,并傳遞給JUnitCore以實現(xiàn)自己特定的需求。
在現(xiàn)實中有可能存在這樣的一個需求,即用戶想同時運行多個測試類,而且這些測試類之間又是相互獨立的,在JUnit中,使用Suite來表達這個需求,但是在Request中并沒有一個單獨的Request接收多個Class實例以創(chuàng)建Suite這個Runner,而是它使用了一個獨立的類Computer來完成這樣的功能,在Request中定義一個靜態(tài)方法來處理這個問題。不過我很奇怪為什么要這么做,這個設計和現(xiàn)存的編程模型是格格不入的,個人不太贊同。
大體介紹了RunnerBuilder和Request的用途和設計,接下來將詳細介紹他們的源碼實現(xiàn),先從RunnerBuilder開始。
RunnerBuilder
RunnerBuilder的核心就是根據(jù)給出的Class實例,創(chuàng)建相應的Runner。一般創(chuàng)建Runner遵循的邏輯是:
1. 如果Class中有@Ignored注解,那么將創(chuàng)建IgnoredClassRunner,該Runner在運行時什么都不做,只是觸發(fā)testIgnored事件。
2. 如果Class中有@RunWith注解,則使用@RunWith注解中指定的Runner。
3. 如果Class中有靜態(tài)suite()方法的存在,則使用SuiteMethod這個Runner(兼容JUnit3)。
4. 如果Class繼承了TestCase類,則使用JUnit38ClassRunner(兼容JUnit3)。
5. 否則,使用BlockJUnit4ClassRunner(JUnit4中默認的Runner)。
其實這個邏輯就是AllDefaultPossibilitiesBuilder中構(gòu)造Runner的實現(xiàn)。在JUnit內(nèi)部大量的使用這個RunnerBuilder來構(gòu)造Runner。其源碼如下:
1 public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
2 private final boolean fCanUseSuiteMethod;
3 public AllDefaultPossibilitiesBuilder(boolean canUseSuiteMethod) {
4 fCanUseSuiteMethod= canUseSuiteMethod;
5 }
6 @Override
7 public Runner runnerForClass(Class<?> testClass) throws Throwable {
8 List<RunnerBuilder> builders= Arrays.asList(
9 ignoredBuilder(),
10 annotatedBuilder(),
11 suiteMethodBuilder(),
12 junit3Builder(),
13 junit4Builder());
14 for (RunnerBuilder each : builders) {
15 Runner runner= each.safeRunnerForClass(testClass);
16 if (runner != null)
17 return runner;
18 }
19 return null;
20 }
21 protected JUnit4Builder junit4Builder() {
22 return new JUnit4Builder();
23 }
24 protected JUnit3Builder junit3Builder() {
25 return new JUnit3Builder();
26 }
27 protected AnnotatedBuilder annotatedBuilder() {
28 return new AnnotatedBuilder(this);
29 }
30 protected IgnoredBuilder ignoredBuilder() {
31 return new IgnoredBuilder();
32 }
33 protected RunnerBuilder suiteMethodBuilder() {
34 if (fCanUseSuiteMethod)
35 return new SuiteMethodBuilder();
36 return new NullBuilder();
37 }
38 }
其中fCanUseSuiteMethod用于表達測試類中靜態(tài)的suite()方法是否被視為用于獲得多個實例運行的方法,這個是為了兼容JUnit3而存在,而且在JUnit內(nèi)部的使用時一般都是給true。再加上junit3Builder()放在junit4Builder()之前構(gòu)造RunnerBuilder,表明為了兼容JUnit3,JUnit4以JUnit3中的風格為首選風格。
這里將不對為了兼容JUnit3而創(chuàng)建的RunnerBuilder做介紹,因而下面只會介紹IgnoredBuilder、AnnotatedBuilder、JUnit4Builder,事實上它都太簡單了,以至于基本上不用什么介紹了。IgnoredBuilder會檢查傳入的Class實例是否有@Ignored注解,若有,則創(chuàng)建IgnoredClassRunner,否則返回null;AnnotatedBuilder檢查傳入的Class實例是否有@RunWith注解,若有,則使用@RunWith注解中指定的Runner,否則,返回null,這里需要注意的是在用戶自定義的Runner中,必須包含一個以Class實例作為參數(shù)的構(gòu)造函數(shù),或者以Class實例和RunnerBuilder實例作為參數(shù)的構(gòu)造函數(shù),否則在構(gòu)造自定義的Runner時會出錯;JUnit4Builder直接根據(jù)傳入的測試類Class的實例創(chuàng)建BlockJUnit4ClassRunner。
1 public class IgnoredBuilder extends RunnerBuilder {
2 @Override
3 public Runner runnerForClass(Class<?> testClass) {
4 if (testClass.getAnnotation(Ignore.class) != null)
5 return new IgnoredClassRunner(testClass);
6 return null;
7 }
8 }
9 public class AnnotatedBuilder extends RunnerBuilder {
10 private static final String CONSTRUCTOR_ERROR_FORMAT= "Custom runner class %s should have a public constructor with signature %s(Class testClass)";
11 private RunnerBuilder fSuiteBuilder;
12 public AnnotatedBuilder(RunnerBuilder suiteBuilder) {
13 fSuiteBuilder= suiteBuilder;
14 }
15 @Override
16 public Runner runnerForClass(Class<?> testClass) throws Exception {
17 RunWith annotation= testClass.getAnnotation(RunWith.class);
18 if (annotation != null)
19 return buildRunner(annotation.value(), testClass);
20 return null;
21 }
22 public Runner buildRunner(Class<? extends Runner> runnerClass,
23 Class<?> testClass) throws Exception {
24 try {
25 return runnerClass.getConstructor(Class.class).newInstance(
26 new Object[] { testClass });
27 } catch (NoSuchMethodException e) {
28 try {
29 return runnerClass.getConstructor(Class.class,
30 RunnerBuilder.class).newInstance(
31 new Object[] { testClass, fSuiteBuilder });
32 } catch (NoSuchMethodException e2) {
33 String simpleName= runnerClass.getSimpleName();
34 throw new InitializationError(String.format(
35 CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName));
36 }
37 }
38 }
39 }
40 public class JUnit4Builder extends RunnerBuilder {
41 @Override
42 public Runner runnerForClass(Class<?> testClass) throws Throwable {
43 return new BlockJUnit4ClassRunner(testClass);
44 }
45 }
而RunBuilder類本身也定義了一些方法,以幫助其他Runner,如Suite,構(gòu)建其內(nèi)部通過其他方式取到的測試類Class實例。這里對parent字段的存在有必要解釋一下,因為我剛開始看到的時候也很費解,addParent()方法只在一個方法中調(diào)用一次,而且就這個類來看也不存在遞歸,為什么會有對相同parents的驗證?要解釋這個問題,需要知道Suite的構(gòu)造函數(shù)還會調(diào)用runners()方法,加入有一次調(diào)用parent為一個使用Suite的類,這個類同時又在children中出現(xiàn),那么在調(diào)用該方法使將給類加入到parents中,而后在構(gòu)造children中的該類時又會調(diào)用該方法,將想用的Class實例加入parents中,從而引起異常。
1 public abstract class RunnerBuilder {
2 private final Set<Class<?>> parents= new HashSet<Class<?>>();
3 public abstract Runner runnerForClass(Class<?> testClass) throws Throwable;
4 public Runner safeRunnerForClass(Class<?> testClass) {
5 try {
6 return runnerForClass(testClass);
7 } catch (Throwable e) {
8 return new ErrorReportingRunner(testClass, e);
9 }
10 }
11 Class<?> addParent(Class<?> parent) throws InitializationError {
12 if (!parents.add(parent))
13 throw new InitializationError(String.format("class '%s' (possibly indirectly) contains itself as a SuiteClass", parent.getName()));
14 return parent;
15 }
16 void removeParent(Class<?> klass) {
17 parents.remove(klass);
18 }
19 public List<Runner> runners(Class<?> parent, Class<?>[] children)
20 throws InitializationError {
21 addParent(parent);
22 try {
23 return runners(children);
24 } finally {
25 removeParent(parent);
26 }
27 }
28 public List<Runner> runners(Class<?> parent, List<Class<?>> children)
29 throws InitializationError {
30 return runners(parent, children.toArray(new Class<?>[0]));
31 }
32 private List<Runner> runners(Class<?>[] children) {
33 ArrayList<Runner> runners= new ArrayList<Runner>();
34 for (Class<?> each : children) {
35 Runner childRunner= safeRunnerForClass(each);
36 if (childRunner != null)
37 runners.add(childRunner);
38 }
39 return runners;
40 }
41 }
最后來看一下RunnerBuilder的類結(jié)構(gòu)圖吧,了解一下目前存在的幾個RunnerBuilder。

Request
Request是對RunnerBuilder的封裝,它提供了改變RunnerBuilder創(chuàng)建出的Runner的接口,如創(chuàng)建Runner后,用Filter或Sorter過濾或重新排列測試方法的順序。就目前JUnit只有Filter和Sorter可以對Runner做一些自定義的配置。Filter可以定義那些測試方法是可以運行的,比如在eclipse中提供的對一個測試方法單獨運行就是使用它來實現(xiàn);或者用戶可以自己定義一個可以運行方法的集合,然后只要遇到這樣的方法,然后根據(jù)這個集合來編寫自定義的Filter。Sorter則用于排列Runner內(nèi)部測試方法的執(zhí)行順序,但是這個定制只是對一個Runner中的測試方法有用,它并不會排列跨Runner之間的測試方法。不廢話了,先來看一下Request的類結(jié)構(gòu)圖吧。

Request的結(jié)構(gòu)比較簡單,而且代碼實現(xiàn)也比較簡單,Request是一個抽象類,它定義了一個getRunner()的抽象方法,這個方法只是返回一個Runner實例。其中ClassRequest根據(jù)一個測試類,使用AllDefaultPossibilitiesBuilder創(chuàng)建一個Runner;FilterRequest則以一個Request和Filter實例為構(gòu)造參數(shù),在實現(xiàn)getRunner()方法時,根據(jù)傳入的Request獲取Runner,并對改Runner應用傳入的Filter以過濾掉那些不需要運行的測試方法;SortingRequest也是以一個Request和Comparator<Description>為構(gòu)造參數(shù),在實現(xiàn)getRunner()方法是,根據(jù)傳入的Request獲取Runner,并根據(jù)comparator構(gòu)造Sorter對剛獲取到的Runner排序。這些實現(xiàn)有點Decorator模式的味道。
1 public class ClassRequest extends Request {
2 private final Class<?> fTestClass;
3 private boolean fCanUseSuiteMethod;
4 public ClassRequest(Class<?> testClass, boolean canUseSuiteMethod) {
5 fTestClass= testClass;
6 fCanUseSuiteMethod= canUseSuiteMethod;
7 }
8 public ClassRequest(Class<?> testClass) {
9 this(testClass, true);
10 }
11 @Override
12 public Runner getRunner() {
13 return new AllDefaultPossibilitiesBuilder(fCanUseSuiteMethod).safeRunnerForClass(fTestClass);
15 }
16 }
17 public final class FilterRequest extends Request {
18 private final Request fRequest;
19 private final Filter fFilter;
20 public FilterRequest(Request classRequest, Filter filter) {
21 fRequest= classRequest;
22 fFilter= filter;
23 }
24 @Override
25 public Runner getRunner() {
26 try {
27 Runner runner= fRequest.getRunner();
28 fFilter.apply(runner);
29 return runner;
30 } catch (NoTestsRemainException e) {
31 return new ErrorReportingRunner(Filter.class, new Exception(String
32 .format("No tests found matching %s from %s", fFilter
33 .describe(), fRequest.toString())));
34 }
35 }
36 }
37 public class SortingRequest extends Request {
38 private final Request fRequest;
39 private final Comparator<Description> fComparator;
40 public SortingRequest(Request request, Comparator<Description> comparator) {
41 fRequest= request;
42 fComparator= comparator;
43 }
44 @Override
45 public Runner getRunner() {
46 Runner runner= fRequest.getRunner();
47 new Sorter(fComparator).apply(runner);
48 return runner;
49 }
50 }
51 public abstract class Request {
52 public abstract Runner getRunner();
53 }
除了Request類結(jié)構(gòu),Request類本身還提供了多個工場方法,以一種不需要知道Request類結(jié)構(gòu)的方法創(chuàng)建Request,也算是一種封裝吧,使用起來比較方便,而且隨著框架的演化,可以添加或刪除子類而不需要考慮用戶是否使用了某個子類。如果做的安全一些、然后不考慮測試的話,可以把FilterRequest和SortingRequest的可見性降低,如包級別的。除了一些靜態(tài)的工場方法,Request為Filter和Sorter也提供了各自的方法支持,在我們得到一個Request的引用后,只需要調(diào)用這兩個方法即可構(gòu)造需要的Request(FilterRequest或SortingRequest)。
1 public abstract class Request {
2 public static Request method(Class<?> clazz, String methodName) {
3 Description method= Description.createTestDescription(clazz, methodName);
4 return Request.aClass(clazz).filterWith(method);
5 }
6 public static Request aClass(Class<?> clazz) {
7 return new ClassRequest(clazz);
8 }
9 public static Request classWithoutSuiteMethod(Class<?> clazz) {
10 return new ClassRequest(clazz, false);
11 }
12 public static Request classes(Computer computer, Class<?>
classes) {
13 try {
14 AllDefaultPossibilitiesBuilder builder= new AllDefaultPossibilitiesBuilder(true);
15 Runner suite= computer.getSuite(builder, classes);
16 return runner(suite);
17 } catch (InitializationError e) {
18 throw new RuntimeException(
19 "Bug in saff's brain: Suite constructor, called as above, should always complete");
20 }
21 }
22 public static Request classes(Class<?>
classes) {
23 return classes(JUnitCore.defaultComputer(), classes);
24 }
25 public static Request runner(final Runner runner) {
26 return new Request(){
27 @Override
28 public Runner getRunner() {
29 return runner;
30 }
31 };
32 }
33 public Request filterWith(Filter filter) {
34 return new FilterRequest(this, filter);
35 }
36 public Request filterWith(final Description desiredDescription) {
37 return filterWith(Filter.matchMethodDescription(desiredDescription));
38 }
39 public Request sortWith(Comparator<Description> comparator) {
40 return new SortingRequest(this, comparator);
41 }
42 }
43 public class Computer {
44 public static Computer serial() {
45 return new Computer();
46 }
47 public Runner getSuite(final RunnerBuilder builder,
48 Class<?>[] classes) throws InitializationError {
49 return new Suite(new RunnerBuilder() {
50 @Override
51 public Runner runnerForClass(Class<?> testClass) throws Throwable {
52 return getRunner(builder, testClass);
53 }
54 }, classes);
55 }
56 protected Runner getRunner(RunnerBuilder builder, Class<?> testClass) throws Throwable {
57 return builder.runnerForClass(testClass);
58 }
59 }
這里對Computer這個類引入的意義一直沒有弄明白,為什么不直接在Request.classes()方法中創(chuàng)建Suite?即使要提取到Computer中以給創(chuàng)建Runner提供擴展點,直接在getSuite()方法中使用builder創(chuàng)建Suite就可以了啊,但是又要將getRunner()方法提取出來,這個提取可以給子類在根據(jù)builder和testClass創(chuàng)建Runner提供擴展點,但是以我目前的水平,還是看不多這個擴展點存在的意義。
JUnitCore
JUnitCore是JUnit中運行Request的門面類,同時它也提供了對命令模式的測試實現(xiàn),它接收多個測試類作為參數(shù),然后運行這些測試類中的所有測試方法。其實現(xiàn)是從傳入的參數(shù)中取到所有的測試類的Class實例,然后根據(jù)這些測試類的Class實例創(chuàng)建Request實例,從創(chuàng)建的Request實例中可以取得Runner實例,運行該Runner,并處理事件邏輯,最后如果所有測試通過,則退出值為0,否則為1。為了統(tǒng)計測試結(jié)果信息,JUnit還提供了一個默認的RunListener實現(xiàn):TextRunListener,這個Listener在每個測試方法開始的時候打印一個點’.’,當一個測試方法失敗是打印E,當一個測試方法被忽略時打印I,當所有測試方法執(zhí)行完成后打印總體統(tǒng)計時間,如運行時間、所有錯誤信息的異常堆棧以及最后成功多少、失敗多少等信息。對于基于JUnit編寫更適合項目本身的測試運行的用戶來說,最重要的就是幾個run()方法,這些用戶可以通過實現(xiàn)自己特定的邏輯以創(chuàng)建出符合自己需求的Request或通過某種方式查找到所有自己要運行的測試類等,然后調(diào)用你需要的run()方法。
1 public class JUnitCore {
2 private RunNotifier fNotifier;
3 public JUnitCore() {
4 fNotifier= new RunNotifier();
5 }
6 public static Result runClasses(Computer computer, Class<?>
classes) {
7 return new JUnitCore().run(computer, classes);
8 }
9 public static Result runClasses(Class<?>
classes) {
10 return new JUnitCore().run(defaultComputer(), classes);
11 }
12 public Result run(Class<?>
classes) {
13 return run(Request.classes(defaultComputer(), classes));
14 }
15 public Result run(Computer computer, Class<?>
classes) {
16 return run(Request.classes(computer, classes));
17 }
18 public Result run(Request request) {
19 return run(request.getRunner());
20 }
21 public Result run(Runner runner) {
22 Result result= new Result();
23 RunListener listener= result.createListener();
24 fNotifier.addFirstListener(listener);
25 try {
26 fNotifier.fireTestRunStarted(runner.getDescription());
27 runner.run(fNotifier);
28 fNotifier.fireTestRunFinished(result);
29 } finally {
30 removeListener(listener);
31 }
32 return result;
33 }
34 }