深入JUnit源码之Rule

简介:
初次用文字的方式记录读源码的过程,不知道怎么写,感觉有点贴代码的嫌疑。不过中间还是加入了一些自己的理解和心得,希望以后能够慢慢的改进,感兴趣的童鞋凑合着看吧,感觉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注解的字段还必须是publicstatic,并且@ClassRule注解的字段在运行时不可以抛异常,不然JUnit的行为是未定义的,这个是注释文档中这样描述的,实际情况则一般是直接触发testFailure事件,至于其他结果,则要看不同的TestRule实现不同,这个特征将在下面详细讲解;而对@Rule注解的字段必须是public,非static,关于@ClassRule注解字段和@Rule注解字段的验证是在RuleFieldValidator中做的(具体可以参考Runner小节)

 1 public enum RuleFieldValidator {
 2     CLASS_RULE_VALIDATOR(ClassRule.classtrue), RULE_VALIDATOR(Rule.classfalse);
 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那节中也已经简单的做过介绍了。

在学一个新的框架的时候,我一直比较喜欢先看一下框架的类图,这样自己总体上就有个概念了。这里也先给一张JUnitTestRule的类图吧:

 

TestRule的类结构图还是比较简单的,只是将它置于JUnitStatement框架中,有些问题分析起来就比较复杂了。为了保持问题的简单,我们先来看一下每个单独的类各自实现了什么功能和怎么实现吧。

TestWatcherTestName

先来看两个简单的吧,TestWatcher为子类提供了四个事件方法以监控测试方法在运行过程中的状态,一般它可以作为信息记录使用。如果TestWatcher作为@ClassRule注解字段,则该测试类在运行之前(调用所有的@BeforeClass注解方法之前)会调用starting()方法;当所有@AfterClass注解方法调用结束后,succeeded()方法会被调用;若@AfterClass注解方法中出现异常,则failed()方法会被调用;最后,finished()方法会被调用;所有这些方法的DescriptionRunner对应的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的参数是RunnerDescription实例,一般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 }

 

ExternalResourceTemporaryFolder

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 }

 

VerifierErrorCollector

Verifier是在所有测试已经结束的时候,再加入一些额外的逻辑,如果额外的逻辑通过,才表示测试成功,否则,测试依旧失败,即使在之前的运行中都是成功的。Verify可以为一些很多测试方法加入一些公共的验证逻辑。当Verifier应用在@Rule注解字段中,它在所偶@After注解方法运行完后,会调用verify()方法,如果verifier()方法验证失败抛出异常,则该测试方法的testFailure事件将会被触发,导致该测试方法失败;当Verifier应用在@ClassRule时,它在所有的@AfterClass注解的方法执行完后,会执行verify()方法,如果verify失败抛出异常,将会触发关于该测试类的testFailure,此时测试类中的所有测试方法都已经运行成功了,却在最后收到一个关于测试类的testFailure事件,这确实是一个比较诡异的事情,因而@ClassRule中提到ErrorCollectorVerifier)不可以用在@ClassRule注解中,否则其行为为定义;更一般的@ClassRule注解的字段运行时不能抛异常,不然其行为是未定义的。

ErrorCollector是对Verifier的一个实现,它可以在运行测试方法的过程中收集错误信息,而这些错误信息知道最后调用ErrorCollectorverify()方法时再处理。其实就目前来看,我很难想象这个需求存在的意义,因为即使它将所有的错误信息收集在一起了,在事件发布是,它还是会为每个错误发布一次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 }

 

TimeoutExpectedException

TimeoutExpectedException都是对@Test注解中timeoutexpected字段的部分替代实现。而且不同于@Test中的注解只适用于单个测试方法,这两个实现适用于全局测试类。对Timeout来说,如果不是在测试类中所有的测试方法都需要有时间限制,我并不推荐适用Timeout;对ExpectedException,它使用了hamcrest中的Matcher来匹配,因而提供了更强大的控制能力,但是一般的使用,感觉@Test中的expected字段就够了,它多次调用expected表达是and的关系,即如果我有两个Exception,则抛出的Exception必须同时是这两个类型的,感觉没有什么大的意义,因而我不怎么推荐使用这个Rule,关于hamcrestMather框架将在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

 

由于TestRuleapply()方法是根据的当前传入的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 }

 

TestRuleStatement的运行

TestRule实例的运行都是被封装在一个叫RunRulesStatement中运行的。在构造RunRules实例是,传入TestRule实例的集合,然后遍历所有的TestRule实例,为每个TestRule实例调用一遍apply()方法以构造出要执行TestRuleStatement链。类似上小节的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 }

 


相关文章
使用Java JUnit框架里的@Rule注解的用法举例
Suppose you need to repeatedly execute some test method in your unit test case, for example, you would like to test getPrice based on the first set of test data 5 times in test method test1() while for the second set of test data, only one time should be executed. The below class RepeatDemoOne is a
使用Java JUnit框架里的@Rule注解的用法举例
使用Java JUnit框架里的@Rule注解的用法举例
使用Java JUnit框架里的@Rule注解的用法举例
198 0
使用Java JUnit框架里的@Rule注解的用法举例
|
测试技术
关于Junit源码的一些探索
关于Junit源码的一些探讨
1298 0
|
设计模式 自然语言处理
|
设计模式 前端开发
|
Java 测试技术 Android开发
|
30天前
|
IDE Java 测试技术
Junit 单元测试
JUnit是Java常用的单元测试框架,简化了测试用例的编写和执行。其特点包括简单注解、自动化测试、可扩展性、灵活性及与IDE的集成。使用方法涉及创建测试类、利用注解如@Test、@BeforeEach等管理测试生命周期,以及使用各种断言方法验证结果。此外,JUnit支持参数化测试以覆盖更多输入组合,并能与Maven、Gradle等构建工具集成,提升测试效率和项目管理。
36 1
|
3月前
|
Java 测试技术
JUnit 4 单元测试
单元测试是软件开发中必不可少的一环,但是在平常开发中往往因为项目周期紧,工作量大而被选择忽略,这样往往导致软件问题层出不穷。 线上出现的不少问题其实在有单元测试的情况下就可以及时发现和处理,因此培养自己在日常开发中写单元测试的能力是很有必要的。无论是对自己的编码能力的提高,还是项目质量的提升,都是大有好处,本文将介绍 Java 单元测试框架 JUnit 4 的基础认识和使用来编写单元测试,希望同样对你有所帮助。