深入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小节):
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。
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还提供了几个方法以在新创建的临时文件夹中创建新的文件、文件夹。
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小节中详细介绍。
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中的,如果要测试一个测试类整体运行时间的话,当然如果存在这种需求的话。
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。如其注释文档中给出的一个例子:
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 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位置不变。
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()方法的执行子类要比父类要早的特性。
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 }