深入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注解的字段还必须是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 }

 

时间: 2024-09-18 22:15:01

深入JUnit源码之Rule的相关文章

深入JUnit源码之Statement

初次用文字的方式记录读源码的过程,不知道怎么写,感觉有点贴代码的嫌疑.不过中间还是加入了一些自己的理解和心得,希望以后能够慢慢的改进,感兴趣的童鞋凑合着看吧,感觉JUnit这个框架还是值得看的,里面有许多不错的设计思想在,更何况它是Kent Beck和Erich Gamma这样的大师写的..... 深入JUnit源码之Statement 看JUnit源码最大的收获就是看到这个Statement的设计,它也是我看到过的所有源码中最喜欢的设计之一.JUnit中Runner的运行过程就是Stateme

深入JUnit源码之Runner

初次用文字的方式记录读源码的过程,不知道怎么写,感觉有点贴代码的嫌疑.不过中间还是加入了一些自己的理解和心得,希望以后能够慢慢的改进,感兴趣的童鞋凑合着看吧,感觉JUnit这个框架还是值得看的,里面有许多不错的设计思想在,更何况它是Kent Beck和Erich Gamma这样的大师写的..... 写在前面的话 不知道是因为第一份工作的影响还是受在博客园上看到的那句"源代码里没有秘密"的影响,总之,近来对很多框架的源码都很感兴趣,拿到一个都想看看.其实自从学习Java以来也看过不少了,

深入JUnit源码之Assert与Hamcrest

初次用文字的方式记录读源码的过程,不知道怎么写,感觉有点贴代码的嫌疑.不过中间还是加入了一些自己的理解和心得,希望以后能够慢慢的改进,感兴趣的童鞋凑合着看吧,感觉JUnit这个框架还是值得看的,里面有许多不错的设计思想在,更何况它是Kent Beck和Erich Gamma这样的大师写的..... 深入JUnit源码之Assert与Hamcrest 到目前,JUnit4所有的核心源码都已经讲解过了,最后剩下的就是为了兼容性而引入的和JUnit3相关的代码以及Assert中的代码.本节将关注于As

关于Junit源码的一些探索

Junit介绍 Junit 是由 Erich Gamma 和 Kent Beck 编写的一个开源的单元测试框架,主要用于白盒测试.Junit 的设计精简,易学易用,但是功能却非常强大. 在Junit3中,使用时只需继承TestCase类,并且 1. 测试方法名以test开头,返回void,方法中没有参数 2. 使用方法setUp()做初始化工作,使用方法tearDown()做清理工作 3. 使用Assert类的方法来判断测试用例结果 Junit源码分析 Junit的流程图: Junit的完整生命

JUnit源码分析(二)

    在上面我们已经提到了junit.extentions包中的内容TestSetup.来看看整个包的结构吧. 先简要的介绍下包中各个类的功能.ActiveTestSuite对TestSuite进行了改进,使得每个test运行在一个单独的线程里面,并且只到所有的线程都结束了才会结束整个测试.ExceptionTestCase是对TestCase进行的改进,可以方便的判断测试类是否抛出了期望的异常.而剩下的三个类,大概你看的出来是使用了装饰模式来设计的.其中TestDecorator为具体装饰类

JUnit源码分析(三)

三.微观--执行流程与代码风格 来过一遍JUnit的执行流程吧,这样你就能对JUnit有个清晰的认识,虽然作为一个使用者这完全是不必要的.从<JUnit in Action>直接拿来一张JUnit流程图. 哦,也许你看晕了,我来当下导游好了.上面已经提到了TestRunner是BaseTestRunner的子类,在三个不同的ui包中各有一个TestRunner.这里我们仅以junit.textui包中的为例. TestRunner作为入口程序是怎么被启动的呢?习惯了使用容器的我们现在也许很少考

JUnit源码分析 (三)——Template Method模式

在JUnit执行测试时,我们经常需要初始化一些环境供测试代码使用,比如数据库连接.mock对象等等,这些初始化代码应当在每一个测试之前执行并在测试方法运行后清理.在JUnit里面就是相应的setUp和tearDown方法.如果没有这两个方法,那么我们要在每个测试方法的代码内写上一大堆重复的初始化和清理代码,这是多么愚蠢的做法.那么JUnit是怎么让setUp和tearDown在测试执行前后被调用的呢?     如果你查看下TestCase方法,你会发现TestCase和TestSuite的run

JUnit源码分析(四)——从Decorator模式说起

其实我这系列小文,名为源码分析,其实是自己读<设计模式>的读书笔记.Decorator模式在java的IO库中得到应用,java的IO库看起来复杂,其实理解了Decorator模式再回头看可以很好理解并使用.     Decorator模式,也就是装饰器模式,是对象结构型模式之一. 1.意图:动态地给一个对象添加一些额外的职责.给对象添加功能,我们首先想到的是继承,但是如果每增一个功能都需要继承,类的继承体系将无可避免地变的庞大和难以理解.面向对象设计的原则:优先使用组合,而非继承,继承的层次

JUnit源码分析(二)——观察者模式

    我们知道JUnit支持不同的使用方式:swt.swing的UI方式,甚至控制台方式,那么对于这些不同的UI我们如何提供统一的接口供它们获取测试过程的信息(比如出现的异常信息,测试成功,测试失败的代码行数等等)?我们试想一下这个场景,当一个error或者exception产生的时候,测试能够马上通知这些UI客户端:发生错误了,发生了什么错误,错误是什么等等.显而易见,这是一个订阅-发布机制应用的场景,应当使用观察者模式.那么什么是观察者模式呢? 观察者模式(Observer) Observ