初次用文字的方式記錄讀源碼的過程,不知道怎么寫,感覺有點貼代碼的嫌疑。不過中間還是加入了一些自己的理解和心得,希望以后能夠慢慢的改進,感興趣的童鞋湊合著看吧,感覺JUnit這個框架還是值得看的,里面有許多不錯的設計思想在,更何況它是Kent Beck和Erich Gamma這樣的大師寫的。。。。。
深入JUnit源碼之Statement
看JUnit源碼最大的收獲就是看到這個Statement的設計,它也是我看到過的所有源碼中最喜歡的設計之一。JUnit中Runner的運行過程就是Statement鏈的運行過程,Statement是對一個單元運行的封裝,每個Statement都只是執行它本身所表達的邏輯,而將其他邏輯交給下一個Statement處理,而且基本上的Statement都存在對下一個節點的引用,從而由此構成一條Statement的鏈,從設計模式的角度上來看,這是一個職責連模式(Chain Of Responsibility Pattern)。JUnit中對@BeforeClass、@AfterClass、@Before、@After、@ClassRule、@Rule等邏輯就是通過Statement來實現的。首先來看一下Statement的類結構。

Statement的類結構還是比較簡單的,首先Statement是所有類的父類,它只定義了一個抽象的evaluate()方法,由其他子類實現該方法;而且除了Fail和InvokeMethod類,其他類都有一個對Statement本身的引用。其實從實現上,每個Statement也是比較簡單的,這個接下來就可以看到了。每個Statement都只實現它自己的邏輯,而將其他邏輯代理給另一個Statement執行,這樣可以在編寫每個Statement的時候只關注自己的邏輯,從而保持Statement本身的簡單,并且易于擴展,當一條Statement執行完后,整個邏輯也就執行完了。不過Statement這條鏈也不是憑空產生的,它也是要根據一定的邏輯構造起來的,關于Statement鏈的構造在JUnit中由Runner負責,為了保持本文的完整性,本文會首先會講解上述幾個Statement的源碼,同時簡單回顧Statement鏈的構造過程,最后將通過一個簡單的例子,將Statement的執行過程用序列圖的方式表達出來,以更加清晰的表達Statement的執行過程。不過本文不會詳細介紹Rules相關的代碼,這部分的代碼將會在下一節詳細介紹。
RunBefores和RunAfters
這兩個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 
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 
14 Statement statement= methodInvoker(method, test);
15 
16 statement= withBefores(method, test, statement);
17 
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 } @AfterClass和@After
這兩個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 {
2 private final Statement fNext;
3 private final Object fTarget;
4 private final List<FrameworkMethod> fAfters;
5 public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {
6 fNext= next;
7 fAfters= afters;
8 fTarget= target;
9 }
10 @Override
11 public void evaluate() throws Throwable {
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)。
1 protected Statement classBlock(final RunNotifier notifier) {
2 
3 statement= withBeforeClasses(statement);
4 statement= withBeforeClasses(statement);
5 
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 
15 statement= withBefores(method, test, statement);
16 statement= withAfters(method, test, statement);
17 
18 }
19 protected Statement withAfters(FrameworkMethod method, Object target,
20 Statement statement) {
21 List<FrameworkMethod> afters= getTestClass().getAnnotatedMethods(
22 After.class);
23 return afters.isEmpty() ? statement : new RunAfters(statement, afters,
24 target);
25 } InvokeMethod、ExpectedException和FailOnTimeout
之所有要把這三個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 }
12 }
使用一個方法實例和測試類的實例構造InvokeMethod,在運行時直接調用該方法。并且InvokeMethod并沒有對其他Statement的引用,因而它是Statement鏈上的最后一個節點。
1 protected Statement methodBlock(FrameworkMethod method) {
2 
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5 statement= withPotentialTimeout(method, test, statement);
6 
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;
7 }
8 @Override
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實例執行后,判斷其拋出的異常是否為指定的異常或者該測試方法沒有拋出異常,在這兩種情況下,測試都會失敗,因而需要它拋出異常以處理這種情況。
1 protected Statement methodBlock(FrameworkMethod method) {
2 
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5 
6 }
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;
12 } 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;
7 }
8 @Override
9 public void evaluate() throws Throwable {
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);
27 }
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類似:
1 protected Statement methodBlock(FrameworkMethod method) {
2 
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5 statement= withPotentialTimeout(method, test, statement);
6 
7 }
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;
12 } Fail與RunRules
Fail這個Statement是在創建測試類出錯時構造的Statement,這個設計思想有點類似Null Object模式,即保持編程模型的統一,即使在出錯的時候也用一個Statement去封裝,這也正是Runner中ErrorReportingRunner的設計思想。
Fail這個Statement很簡單,它在運行時重新拋出之前記錄的異常,其構造過程也是在創建測試類實例出錯時構造:
1 protected Statement methodBlock(FrameworkMethod method) {
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 }
13 
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();
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 }
17 protected Statement classBlock(final RunNotifier notifier) {
18 
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());
27 }
28 protected Statement methodBlock(FrameworkMethod method) {
29 
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注解的方法要遲。
Statement執行序列圖
假設在一個測試類中存在多個@BeforeClass、@AfterClass、@Before、@After注解的方法,并且有兩個測試方法testMethod1()和testMethod2(),那么他們的運行序列圖如下所示(這里沒有考慮Rule,對@ClassRule的RunRules,它的鏈在最前端,而@Rule的RunRules則是在RunAfters的前面,關于Rule將會在下一節中詳細講解):
