JUnit 框架阅读一

Junit 基础元素

最近阅读了JUnit 的源代码,记录一下。首先我画了一张JUnit 基础元素图如下:

junitFirst.png

基本上涵盖了JUnit 的核心组件和类。下面我们会选一部分比较重要的元素来作一些解释。

Runner

首先是 Runner 类,Runner是用来运行测试的主要类,下图是Runner的继承结构:


image.png

其中 ParentRunner是一个比较重要的Runner,里面承载了很多测试业务的代码。读者可以详细阅读 ParentRunner。好了,我们看一下 Runner的抽象基类:

public abstract class Runner implements Describable {
    /*
     * (non-Javadoc)
     * @see org.junit.runner.Describable#getDescription()
     */
    public abstract Description getDescription();

    /**
     * Run the tests for this runner.
     *
     * @param notifier will be notified of events while tests are being run--tests being
     * started, finishing, and failing
     */
    public abstract void run(RunNotifier notifier);

    /**
     * @return the number of tests to be run by the receiver
     */
    public int testCount() {
        return getDescription().testCount();
    }
}

其中的 run 方法是用来跑测试的方法,run 方法中有一个 RunNotifier 的参数,主要是用来通知测试的生命周期我们下面会提到它,以 ParentRunner来作例子,run 方法如下:

public abstract class ParentRunner<T> extends Runner implements Filterable,
        Orderable {
  private final TestClass testClass;
  ...
  @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        testNotifier.fireTestSuiteStarted();
        try {
            Statement statement = classBlock(notifier);
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        } finally {
            testNotifier.fireTestSuiteFinished();
        }
    }
    ...
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
            statement = withInterruptIsolation(statement);
        }
        return statement;
    }
    ...
}

在上述代码中 testClass 是类型为 TestClass的对象,包装了被测试类,后面会提到。可以看到,在run方法中实际上是运行 Statement 的evaluate,主要使用了责任链的设计模式,Runner 会创建一系列的 Statement,以BlockJUnit4ClassRunner为例子:

protected Statement methodBlock(final FrameworkMethod method) {
       ...
        Statement statement = methodInvoker(method, test);
        statement = possiblyExpectingExceptions(method, test, statement);
        statement = withPotentialTimeout(method, test, statement);
        statement = withBefores(method, test, statement);
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        statement = withInterruptIsolation(statement);
        return statement;
    }

可以看到我们测试时候常用的一些生命周期,比如 @Before, @After 以及我们在测试中设置的一些 @get:Rule,都会通过解析Annotation之后生成 statement 来串行的运行。

TestClass

TestClass 实现 Annotatable 接口,提供方法校验和注解搜索的功能。

public interface Annotatable {
    /**
     * Returns the model elements' annotations.
     */
    Annotation[] getAnnotations();

    /**
     * Returns the annotation on the model element of the given type, or @code{null}
     */
    <T extends Annotation> T getAnnotation(Class<T> annotationType);
}

Runner 会invoke其中的getAnnotatedMethods来获取被测试类中的标注方法。以 @BeforeClass 标注为例子:在运行 Runner 的 run 方法时,会调用withBeforeClasses 函数如下:

 protected Statement withBeforeClasses(Statement statement) {
        List<FrameworkMethod> befores = testClass
                .getAnnotatedMethods(BeforeClass.class);
        return befores.isEmpty() ? statement :
                new RunBefores(statement, befores, null);
    }

在这个函数里调用了 getAnnotatedMethods 方法参数是 BeforeClass.class,这样就获取到了被测试类中被 @BeforeClass标注的方法, 然后将其封装在RunBefores类中, RunBefores 是一个Statement的子类其中的evaluate方法会调用个测试类中被@BeforeClasse 的方法。

RunNotifier

RunNotifier 是负责通知测试的生命周期回调。其中有一个私有的抽象内部类 SafeNotifier. 并且提供了一系列的生命周期回调方法,在对应的时机调用:

  public class RunNotifier {
    private final List<RunListener> listeners = new CopyOnWriteArrayList<RunListener>();
    ...
    public void fireTestRunFinished(final Result result) {
        new SafeNotifier() {
            @Override
            protected void notifyListener(RunListener each) throws Exception {
                each.testRunFinished(result);
            }
        }.run();
    }
    ...
    public void fireTestSuiteStarted(final Description description) {
        new SafeNotifier() {
            @Override
            protected void notifyListener(RunListener each) throws Exception {
                each.testSuiteStarted(description);
            }
        }.run();
    }
 }

调用位置以JUnitCore为例子: notifier.fireTestRunStarted(runner.getDescription());

     public Result run(Runner runner) {
        Result result = new Result();
        RunListener listener = result.createListener();
        notifier.addFirstListener(listener);
        try {
            notifier.fireTestRunStarted(runner.getDescription());
            runner.run(notifier);
            notifier.fireTestRunFinished(result);
        } finally {
            removeListener(listener);
        }
        return result;
    }

我们可以看到在开始运行测试的时候,通过调用 notifier 的 fireTestRunStarted 来通知测试开始。

Statement

Statement是一个抽象基类:有一些具体实现类比如 RunAfters, RunRules.

public abstract class Statement {
    /**
     * Run the action, throwing a {@code Throwable} if anything goes wrong.
     */
    public abstract void evaluate() throws Throwable;
}

我们以RunAfters 为例子:

public class RunAfters extends Statement {
   private final Statement next;

   private final Object target;

   private final List<FrameworkMethod> afters;

   public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {
       this.next = next;
       this.afters = afters;
       this.target = target;
   }

   @Override
   public void evaluate() throws Throwable {
       List<Throwable> errors = new ArrayList<Throwable>();
       try {
           next.evaluate();
       } catch (Throwable e) {
           errors.add(e);
       } finally {
           for (FrameworkMethod each : afters) {
               try {
                   invokeMethod(each);
               } catch (Throwable e) {
                   errors.add(e);
               }
           }
       }
       MultipleFailureException.assertEmpty(errors);
   }

   /**
    * @since 4.13
    */
   protected void invokeMethod(FrameworkMethod method) throws Throwable {
       method.invokeExplosively(target);
   }
}

其中 next 指向责任链的下一个节点。不难看出 afters 方法在 finally 代码块中执行,即无论测试是失败还是成功都会run @After 标注的代码。

TestRule

我们在测试的时候经常会遇到需要使用 TestRule 的情况:
比如在测试koltin 的协程代码的时候就会遇到需要添加自定义的Rule:

@get:Rule
    val testCoroutineRule = TestCoroutineRule()

自定义或者JUnit 内部定义的一系例Rule 都是继承TestRule 这个接口:

public interface TestRule {
    /**
     * Modifies the method-running {@link Statement} to implement this
     * test-running rule.
     *
     * @param base The {@link Statement} to be modified
     * @param description A {@link Description} of the test implemented in {@code base}
     * @return a new statement, which may be the same as {@code base},
     *         a wrapper around {@code base}, or a completely new Statement.
     */
    Statement apply(Statement base, Description description);
}

其中的 apply 方法是用来基于 base Statement 加上测试Rule 来创建一个新的Statement用于evaluate。举个例子Verifier:

   public abstract class Verifier implements TestRule {
   public Statement apply(final Statement base, Description description) {
       return new Statement() {
           @Override
           public void evaluate() throws Throwable {
               base.evaluate();
               verify();
           }
       };
   }

   /**
    * Override this to add verification logic. Overrides should throw an
    * exception to indicate that verification failed.
    */
   protected void verify() throws Throwable {
   }
}

我们看到在 Verifier 的 apply 方法中首先调用了 baseStatement的 evaluate 方法,然后调用自己 verify 方法。如何使用呢?我们看一下测试例子。

  public class VerifierRuleTest {

    private static String sequence;

    public static class UsesVerifier {
        @Rule
        public Verifier collector = new Verifier() {
            @Override
            protected void verify() {
                sequence += "verify ";
            }
        };

        @Test
        public void example() {
            sequence += "test ";
        }
    }

    @Test
    public void verifierRunsAfterTest() {
        sequence = "";
        assertThat(testResult(UsesVerifier.class), isSuccessful());
        assertEquals("test verify ", sequence);
    }
}

我们看到测试例子中 assertEquals("test verify ", sequence); 是通过的,结合 Verifier 的代码,我们可以看到verifier 在测试代码运行之后调用。

JUnit的框架代码不是很多,很适合新手开始阅读,所以我的一系列框架阅读之旅就从简单的开始,这一篇是JUnit的基础篇,下一篇会讲到一些具体的使用情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容