深入JUnit源碼之Builder、Request與JUnitCore
經過前面三節的Runner、Statement、Rule的講解,事實上JUnit的核心運行邏輯已經完成了,剩下的就是一些外圍的支持代碼,包括Runner的構建、用Assert對測試方法的運行結果的驗證代碼以及為了兼容性而存在的一些代碼。本節將關注Runner的構建部分,在JUnit中通過Request和RunnerBuilder共同支持。
在JUnit中,RunnerBuilder對根據測試類創建Runner邏輯的封裝,特別是它支持@RunWith注解以在測試類中指定需要的執行的Runner,這也是自定義的Runner可以很方便的插入JUnit框架運行的原因,這個設計其實也蠻具有參考價值的:通過注解的方式為用戶提供插入點,以擴展框架本身的功能;而在實現工程中,通過外圍Builder來支持,從而不影響核心設計。
Request有點類似配置實例的感覺,用戶可以根據自己的需求來定制Runner創建的信息,如代理給RunnerBuilder為每個測試類創建相應的Runner,根據用戶的需求以決定是否要有filter和sort操作等。用戶可以定義自己的Request實例,并傳遞給JUnitCore以實現自己特定的需求。
在現實中有可能存在這樣的一個需求,即用戶想同時運行多個測試類,而且這些測試類之間又是相互獨立的,在JUnit中,使用Suite來表達這個需求,但是在Request中并沒有一個單獨的Request接收多個Class實例以創建Suite這個Runner,而是它使用了一個獨立的類Computer來完成這樣的功能,在Request中定義一個靜態方法來處理這個問題。不過我很奇怪為什么要這么做,這個設計和現存的編程模型是格格不入的,個人不太贊同。
大體介紹了RunnerBuilder和Request的用途和設計,接下來將詳細介紹他們的源碼實現,先從RunnerBuilder開始。
RunnerBuilder
RunnerBuilder的核心就是根據給出的Class實例,創建相應的Runner。一般創建Runner遵循的邏輯是:
1. 如果Class中有@Ignored注解,那么將創建IgnoredClassRunner,該Runner在運行時什么都不做,只是觸發testIgnored事件。
2. 如果Class中有@RunWith注解,則使用@RunWith注解中指定的Runner。
3. 如果Class中有靜態suite()方法的存在,則使用SuiteMethod這個Runner(兼容JUnit3)。
4. 如果Class繼承了TestCase類,則使用JUnit38ClassRunner(兼容JUnit3)。
5. 否則,使用BlockJUnit4ClassRunner(JUnit4中默認的Runner)。
其實這個邏輯就是AllDefaultPossibilitiesBuilder中構造Runner的實現。在JUnit內部大量的使用這個RunnerBuilder來構造Runner。其源碼如下:
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用于表達測試類中靜態的suite()方法是否被視為用于獲得多個實例運行的方法,這個是為了兼容JUnit3而存在,而且在JUnit內部的使用時一般都是給true。再加上junit3Builder()放在junit4Builder()之前構造RunnerBuilder,表明為了兼容JUnit3,JUnit4以JUnit3中的風格為首選風格。
這里將不對為了兼容JUnit3而創建的RunnerBuilder做介紹,因而下面只會介紹IgnoredBuilder、AnnotatedBuilder、JUnit4Builder,事實上它都太簡單了,以至于基本上不用什么介紹了。IgnoredBuilder會檢查傳入的Class實例是否有@Ignored注解,若有,則創建IgnoredClassRunner,否則返回null;AnnotatedBuilder檢查傳入的Class實例是否有@RunWith注解,若有,則使用@RunWith注解中指定的Runner,否則,返回null,這里需要注意的是在用戶自定義的Runner中,必須包含一個以Class實例作為參數的構造函數,或者以Class實例和RunnerBuilder實例作為參數的構造函數,否則在構造自定義的Runner時會出錯;JUnit4Builder直接根據傳入的測試類Class的實例創建BlockJUnit4ClassRunner。
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,構建其內部通過其他方式取到的測試類Class實例。這里對parent字段的存在有必要解釋一下,因為我剛開始看到的時候也很費解,addParent()方法只在一個方法中調用一次,而且就這個類來看也不存在遞歸,為什么會有對相同parents的驗證?要解釋這個問題,需要知道Suite的構造函數還會調用runners()方法,加入有一次調用parent為一個使用Suite的類,這個類同時又在children中出現,那么在調用該方法使將給類加入到parents中,而后在構造children中的該類時又會調用該方法,將想用的Class實例加入parents中,從而引起異常。
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的類結構圖吧,了解一下目前存在的幾個RunnerBuilder。
Request
Request是對RunnerBuilder的封裝,它提供了改變RunnerBuilder創建出的Runner的接口,如創建Runner后,用Filter或Sorter過濾或重新排列測試方法的順序。就目前JUnit只有Filter和Sorter可以對Runner做一些自定義的配置。Filter可以定義那些測試方法是可以運行的,比如在eclipse中提供的對一個測試方法單獨運行就是使用它來實現;或者用戶可以自己定義一個可以運行方法的集合,然后只要遇到這樣的方法,然后根據這個集合來編寫自定義的Filter。Sorter則用于排列Runner內部測試方法的執行順序,但是這個定制只是對一個Runner中的測試方法有用,它并不會排列跨Runner之間的測試方法。不廢話了,先來看一下Request的類結構圖吧。
Request的結構比較簡單,而且代碼實現也比較簡單,Request是一個抽象類,它定義了一個getRunner()的抽象方法,這個方法只是返回一個Runner實例。其中ClassRequest根據一個測試類,使用AllDefaultPossibilitiesBuilder創建一個Runner;FilterRequest則以一個Request和Filter實例為構造參數,在實現getRunner()方法時,根據傳入的Request獲取Runner,并對改Runner應用傳入的Filter以過濾掉那些不需要運行的測試方法;SortingRequest也是以一個Request和Comparator<Description>為構造參數,在實現getRunner()方法是,根據傳入的Request獲取Runner,并根據comparator構造Sorter對剛獲取到的Runner排序。這些實現有點Decorator模式的味道。
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類結構,Request類本身還提供了多個工場方法,以一種不需要知道Request類結構的方法創建Request,也算是一種封裝吧,使用起來比較方便,而且隨著框架的演化,可以添加或刪除子類而不需要考慮用戶是否使用了某個子類。如果做的安全一些、然后不考慮測試的話,可以把FilterRequest和SortingRequest的可見性降低,如包級別的。除了一些靜態的工場方法,Request為Filter和Sorter也提供了各自的方法支持,在我們得到一個Request的引用后,只需要調用這兩個方法即可構造需要的Request(FilterRequest或SortingRequest)。
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<?>

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<?>

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()方法中創建Suite?即使要提取到Computer中以給創建Runner提供擴展點,直接在getSuite()方法中使用builder創建Suite就可以了啊,但是又要將getRunner()方法提取出來,這個提取可以給子類在根據builder和testClass創建Runner提供擴展點,但是以我目前的水平,還是看不多這個擴展點存在的意義。
JUnitCore
JUnitCore是JUnit中運行Request的門面類,同時它也提供了對命令模式的測試實現,它接收多個測試類作為參數,然后運行這些測試類中的所有測試方法。其實現是從傳入的參數中取到所有的測試類的Class實例,然后根據這些測試類的Class實例創建Request實例,從創建的Request實例中可以取得Runner實例,運行該Runner,并處理事件邏輯,最后如果所有測試通過,則退出值為0,否則為1。為了統計測試結果信息,JUnit還提供了一個默認的RunListener實現:TextRunListener,這個Listener在每個測試方法開始的時候打印一個點’.’,當一個測試方法失敗是打印E,當一個測試方法被忽略時打印I,當所有測試方法執行完成后打印總體統計時間,如運行時間、所有錯誤信息的異常堆棧以及最后成功多少、失敗多少等信息。對于基于JUnit編寫更適合項目本身的測試運行的用戶來說,最重要的就是幾個run()方法,這些用戶可以通過實現自己特定的邏輯以創建出符合自己需求的Request或通過某種方式查找到所有自己要運行的測試類等,然后調用你需要的run()方法。
2 private RunNotifier fNotifier;
3 public JUnitCore() {
4 fNotifier= new RunNotifier();
5 }
6 public static Result runClasses(Computer computer, Class<?>

7 return new JUnitCore().run(computer, classes);
8 }
9 public static Result runClasses(Class<?>

10 return new JUnitCore().run(defaultComputer(), classes);
11 }
12 public Result run(Class<?>

13 return run(Request.classes(defaultComputer(), classes));
14 }
15 public Result run(Computer computer, Class<?>

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 }