初次用文字的方式記錄讀源碼的過程,不知道怎么寫,感覺有點貼代碼的嫌疑。不過中間還是加入了一些自己的理解和心得,希望以后能夠慢慢的改進,感興趣的童鞋湊合著看吧,感覺JUnit這個框架還是值得看的,里面有許多不錯的設計思想在,更何況它是Kent Beck和Erich Gamma這樣的大師寫的。。。。。
深入JUnit源碼之Rule
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注解字段的驗證
@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 
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
本節將重點介紹當前JUnit默認實現的幾個TestRule,先給出類圖,然后介紹源碼實現以及用途,最后還將簡單的介紹RunRules這個Statement的運行信息,雖然這個類非常簡單,在Statement那節中也已經簡單的做過介紹了。
在學一個新的框架的時候,我一直比較喜歡先看一下框架的類圖,這樣自己總體上就有個概念了。這里也先給一張JUnit中TestRule的類圖吧:
TestRule的類結構圖還是比較簡單的,只是將它置于JUnit的Statement框架中,有些問題分析起來就比較復雜了。為了保持問題的簡單,我們先來看一下每個單獨的類各自實現了什么功能和怎么實現吧。
TestWatcher和TestName
先來看兩個簡單的吧,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) {
22 }
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 }
ExternalResource與TemporaryFolder
ExternalResource為子類提供了兩個接口,分別是進入測試之前和退出測試之后,一般它是作為對一些資源在測試前后的控制,如Socket的開啟與關閉、Connection的開始與斷開、臨時文件的創建與刪除等。如果ExternalResource用在@ClassRule注解字段中,before()方法會在所有@BeforeClass注解方法之前調用;after()方法會在所有@AfterClass注解方法之后調用,不管在執行@AfterClass注解方法時是否拋出異常。如果ExternalResource用在@Rule注解字段中,before()方法會在所有@Before注解方法之前調用;after()方法會在所有@After注解方法之后調用。
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();
28 }
29 @Override
30 protected void after() {
31 delete();
32 }
33 public void create() throws IOException {
34 folder= newFolder();
35 }
36 public File newFile(String fileName) throws IOException {
37 File file= new File(getRoot(), fileName);
38 file.createNewFile();
39 return file;
40 }
41 public File newFile() throws IOException {
42 return File.createTempFile("junit", null, folder);
43 }
44 public File newFolder(String
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和ErrorCollector
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) {
3 return new Statement() {
4 @Override
5 public void evaluate() throws Throwable {
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);
19 }
20 public void addError(Throwable error) {
21 errors.add(error);
22 }
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 }
Timeout與ExpectedException
Timeout與ExpectedException都是對@Test注解中timeout和expected字段的部分替代實現。而且不同于@Test中的注解只適用于單個測試方法,這兩個實現適用于全局測試類。對Timeout來說,如果不是在測試類中所有的測試方法都需要有時間限制,我并不推薦適用Timeout;對ExpectedException,它使用了hamcrest中的Matcher來匹配,因而提供了更強大的控制能力,但是一般的使用,感覺@Test中的expected字段就夠了,它多次調用expected表達是and的關系,即如果我有兩個Exception,則拋出的Exception必須同時是這兩個類型的,感覺沒有什么大的意義,因而我不怎么推薦使用這個Rule,關于hamcrest的Mather框架將在Assert小節中詳細介紹。這兩個Rule原本就是基于測試方法設計的,因而如果應用在@ClassRule上好像沒有什么大的意義,不過Timeout感覺是可以應用在@ClassRule中的,如果要測試一個測試類整體運行時間的話,當然如果存在這種需求的話。
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);
8 }
9 }
10 public class ExpectedException implements TestRule {
11 public static ExpectedException none() {
12 return new ExpectedException();
13 }
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);
20 }
21 public void expect(Matcher<?> matcher) {
22 if (fMatcher == null)
23 fMatcher= (Matcher<Object>) matcher;
24 else
25 fMatcher= both(fMatcher).and(matcher);
26 }
27 public void expect(Class<? extends Throwable> type) {
28 expect(instanceOf(type));
29 }
30 public void expectMessage(String substring) {
31 expectMessage(containsString(substring));
32 }
33 public void expectMessage(Matcher<String> matcher) {
34 expect(hasMessage(matcher));
35 }
36 private class ExpectedExceptionStatement extends Statement {
37 private final Statement fNext;
38 public ExpectedExceptionStatement(Statement base) {
39 fNext= base;
40 }
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
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);
10 }
11 private RuleChain(List<TestRule> rules) {
12 this.rulesStartingWithInnerMost= rules;
13 }
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);
19 }
20 public Statement apply(Statement base, Description description) {
21 for (TestRule each : rulesStartingWithInnerMost)
22 base= each.apply(base, description);
23 return base;
24 }
25 }
TestRule在Statement的運行
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);
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 }