「译」JUnit 5 系列:条件测试 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
linesh
V2EX    程序员

「译」JUnit 5 系列:条件测试

  •  
  •   linesh
    linesh-simplicity 2016 年 9 月 24 日 2468 次点击
    这是一个创建于 3419 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址:http://blog.codefx.org/libraries/junit-5-conditions/
    原文日期: 08, May, 2016
    译文首发: Linesh 的博客:「译」 JUnit 5 系列:条件测试
    我的 Github :http://github.com/linesh-simplicity

    上一节我们了解了 JUnit 新的扩展模型,了解了它是如何支持我们向引擎定制一些行为的。然后我还预告会为大家讲解条件测试,这一节主题就是它了。

    条件测试,指的是允许我们自定义灵活的标准,来决定一个测试是否应该执行。条件( condition ) 官方的叫法是条件测试执行

    概述

    (如果不喜欢看文章,你可以戳这里看我的演讲,或者看一下最近的 vJUG 讲座,或者我在 DevoxxPL 上的 PPT

    本系列文章都基于 Junit 5 发布的先行版 Milestone 2。它可能会有变化。如果有新的里程碑( milestone )版本发布,或者试用版正式发行时,我会再来更新这篇文章。

    这里要介绍的多数知识你都可以在 JUnit 5 用户指南 中找到(这个链接指向的是先行版 Milestone 2 ,想看的最新版本文档的话请戳这里),并且指南还有更多的内容等待你发掘。下面的所有代码都可以在 我的 Github 上找到。

    目录

    • 相关的扩展点
    • 动手实现一个 @Disabled 注解
    • @DisabledOnOs
      • 一种简单的实现方式
      • 更简洁的 API
      • 代码重构
    • @DisabledIfTestFails
      • 异常收集
      • 禁用测试
      • 集成
    • 回顾总结
    • 分享&关注

    相关的扩展点

    还记得 拓展点 一节讲的内容吗?不记得了?好吧,简单来说, JUnit 5 中定义了许多扩展点,每个扩展点都对应一个接口。你自己的扩展可以实现其中的某些接口,然后通过 @ExtendWith 注解注册给 JUnit ,后者会在特定的时间点调用你的接口实现。

    要实现条件测试,你需要关注其中的两个扩展点: ContainerExecutionCondition (容器执行条件)和 TestExecutionCondition (测试执行条件)。

    public interface ContainerExecutionCondition extends Extension { /** * Evaluate this condition for the supplied ContainerExtensionContext. * * An enabled result indicates that the container should be executed; * whereas, a disabled result indicates that the container should not * be executed. * * @param context the current ContainerExtensionContext; never null * @return the result of evaluating this condition; never null */ ConditionEvaluationResult evaluate(ContainerExtensionContext context); } public interface TestExecutionCondition extends Extension { /** * Evaluate this condition for the supplied TestExtensionContext. * * An enabled result indicates that the test should be executed; * whereas, a disabled result indicates that the test should not * be executed. * * @param context the current TestExtensionContext; never null * @return the result of evaluating this condition; never null */ ConditionEvaluationResult evaluate(TestExtensionContext context); } 

    ContainerExecutionCondition 接口将决定容器中的测试是否会被执行。通常情况下,你使用 @Test 注解来标记测试,此时测试所在的类就是容器。同时,单独的测试方法是否执行则是由 TestExecutionCondition 接口决定的。

    (这里,我说的是“通常情况下”,因为其他测试引擎可能对容器和测试有截然不同的定义。但一般情况下,测试就是单个的方法,容器指的就是测试类。)

    嗯,基本知识就这么多。想实现条件测试,至少需要实现以上两个接口中的一个,并在接口的 evalute 方法中执行自己的条件检查。

    动手实现一个 @Disabled 注解

    最简单的“条件”就是判断都没有,直接禁用测试。如果在方法上发现了 @Disabled 注解,我们就直接禁用该测试。

    让我们来写一个这样的 @Disabled 注解吧:

    @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(@DisabledCondition.class) public @interface Disabled { } 

    对应的扩展如下:

    public class DisabledCondition implements ContainerExecutionCondition, TestExecutionCondition { private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult.enabled("@Disabled is not present"); @Override public ConditionEvaluationResult evaluate( ContainerExtensionContext context) { return evaluateIfAnnotated(context.getElement()); } @Override public ConditionEvaluationResult evaluate( TestExtensionContext context) { return evaluateIfAnnotated(context.getElement()); } private ConditionEvaluationResult evaluateIfAnnotated( Optional<AnnotatedElement> element) { Optional<Disabled> disabled = AnnotationUtils .findAnnotation(element, Disabled.class); if (disabled.isPresent()) return ConditionEvaluationResult .disabled(element + " is @Disabled"); return ENABLED; } } 

    写起来小菜一碟吧?在 JUnit 真实的产品代码中,@Disabled 也是这么实现的。不过,有两个地方有一些细微的差别:

    • 官方 @Disabled 注解不需要再使用 @ExtendWith 注册扩展,因为它是默认注册了的
    • 官方 @Disabled 注解可以接收一个参数,解释测试被忽略的理由。它会在测试被忽略时被记录下来

    使用时请注意,AnnotationUtils 是个内部 API 。不过,官方可能很快就会将它提供的功能给开放出来

    接下来让我们写点更有意思的东西吧。

    @DisabledOnOs

    如果有些测试我们只想让它在特定的操作系统上面运行,这个要怎么实现呢?

    一种简单的实现方式

    当然,我们还是从注解开始咯:

    @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(OsCondition.class) public @interface DisabledOnOs { OS[] value() default {}; } 

    这回注解需要接收一个或多个参数值,你需要告诉它想禁用测试的操作系统有哪些。 OS 是个枚举类,定义了所有操作系统的名字。同时,它还提供了一个静态的 static OS determine() 方法,你可能已经从名字猜到了,它会推断并返回你当前所用的操作系统。

    现在我们可以着手实现 OsCondition 扩展类了。它必须检查两点:注解是否存在,以及当前操作系统是否在注解声明的禁用列表中。

    public class OsCondition implements ContainerExecutionCondition, TestExecutionCondition { // both `evaluate` methods forward to `evaluateIfAnnotated` as above private ConditionEvaluationResult evaluateIfAnnotated( Optional<AnnotatedElement> element){ Optional<DisabledOnOs> disabled = AnnotationUtils .findAnnotation(element, DisabledOnOs.class); if (disabled.isPresent()) return disabledIfOn(disabled.get().value()); return ENABLED; } private ConditionEvaluationResult disabledIfOn(OS[] disabledOnOs) { OS os = OS.determine(); if (Arrays.asList(disabledOnOs).contains(os)) return ConditionEvaluationResult .disabled("Test is disabled on " + os + "."); else return ConditionEvaluationResult .enabled("Test is not disabled on " + os + "."); } } 

    然后使用的时候就可以像这样:

    @Test @DisabledOnOs(OS.WINDOWS) void doesNotRunOnWindows() { assertTrue(false); } 

    棒。

    更简洁的 API

    但代码还可以写得更好!JUnit 的注解是可组合的,基于此我们可以让这个条件注解更简洁:

    @TestExceptOnOs(OS.WINDOWS) void doesNotRunOnWindowsEither() { assertTrue(false); } 

    @TestExceptionOnOs完美的实现方案是这样的:

    @Retention(RetentionPolicy.RUNTIME) @Test @DisabledOnOs(/* 通过某种方式取得注解下的 `value` 值 */) public @interface TestExceptOnOs { OS[] value() default {}; } 

    测试实际运行时, OsCondition::evaluateIfAnnotated 方法会扫描 @DisabledOnOs 注解,然后我们发现它又是对 @TestExceptOnOs 的注解,前面写的代码就可以如期工作了。但我不知道如何在 @DisabledOnOs 注解中获取 @TestExceptOnOs 中的value()值。:((你能做到吗?)

    次佳的选择是,简单地在 @TestExceptOnOs 注解上直接声明应用的扩展就可以了:

    @Retention(RetentionPolicy.RUNTIME) @ExtendWith(OsCondition.class) @Test public @interface TestExceptOnOs { OS[] value() default {}; } 

    然后直接把 OsCondition:evaluateIfAnnotated 方法拉过来改改即可:

    private ConditionEvaluationResult evaluateIfAnnotated( Optional<Annotatedelement> element) { Optional<DisabledOnOs> disabled = AnnotationUtils .findAnnotation(element, DisabledOnOs.class); if (disabled.isPresent()) return disabledIfOn(disabled.get().value()); Optional<TestExceptOnOs> testExcept = AnnotationUtils .findAnnotation(element, TestExceptOnOs.class); if (testExcept.isPresent()) return disabledIfOn(testExcept.get().value()); return ConditionEvaluationResult.enabled(""); } 

    收工。现在我们可以如期使用这个注解了。

    代码重构

    我们还需要创建一个意义刚好相反的注解(即现在变为,当前操作系统不在提供列表时,才禁用测试),工作是类似的,但是注解名会更表意,再加入静态导入后,我们的代码最终可以整理成这样:

    @TestOn(WINDOWS) void doesNotRunOnWindoesEither() { assertTrue(false); } 

    还挺好看的,是不?

    「译者注:英文中 condition 有多个意思:“条件;空调”。作者这里配图取双关」

    @DisabledIfTestFails

    我们再考虑一种场景我保证这次可以接触更有意思的东西!假设现在有许多(集成)测试,如果其中有一个抛出了特定的异常而失败,那么其他测试也必须会挂。为了节省时间,我们希望在这种情况下直接禁用掉其他的测试。

    那么我们需要做些什么工作呢?首先第一反应不难想到,我们 必须先能收集测试执行过程抛出的异常。这肯定需要在单个测试类级别的生命周期中进行处理,否则就可能因为其他测试类中抛出的异常而影响到本测试类的运行。其次,我们需要一个实现一个条件:它会检查某个特定的异常是否已被抛出过,若是,禁用当前测试。

    异常收集

    翻阅一下文档中提供的 扩展点列表,不难发现有一项“异常处理”,看起来就是我们想要的东西:

    /** * TestExecutionExceptionHandler defines the API for Extensions that wish to react to thrown exceptions in tests. * * [ ... ] */ public interface TestExecutionExceptionHandler extends ExtensionPoint { /** * Handle the supplied throwable. * * Implementors must perform one of the following. * * - Swallow the supplied throwable, thereby preventing propagation * - Rethrow the incoming throwable as is * - Throw a new exception, potenially wrapping the supplied throwable * * [ ... ] */ void handleTestExecutionException( TestExtensionContext context, Throwable throwable) throws Throwable; } 

    读完发现,我们的任务就是实现 handleException 方法,存储起接收到的异常并重新抛出。

    你可能还记得我提过的关于扩展点和无状态的一些结论:

    引擎对扩展实例的初始化时间、实例的生存时间未作出任何规约和保证,因此,扩展必须是无状态的。如果一个扩展需要维持任何状态信息,那么它必须使用 JUnit 提供的一个仓库( store )来进行信息读取和写入。

    看来我们是必须使用这个 store 了。 store 其实就是存放我们希望保存的一些东西,一个可索引的资源集合。它可以在扩展上下文对象中取得,后者会被传给大多数扩展点接口方法作为参数。不过需要注意的是,每个不同的上下文对象都有自己一个独立的 store ,所以我们还必须决定使用哪个 store 。

    每个测试方法有一个自己的上下文对象(TestExtensionContext),同时,测试类也有一个自己的上下文对象(ContainerExtensionContext)。还记得我们的需求吗?保存测试类中任何测试方法可能抛出的异常,仅此而已。也即,我们不会保存其他测试类中抛出的异常。这样一来,容器级别的上下文 ContainerExtensionContext 刚好就是我们需要的了。

    接下来,我们可以使用这个容器上下文,通过它来存储所有测试过程抛出的异常:

    private static final Namespece NAMESPACE = Namespace .of("org", "codefx", "CollectExceptions"); private static final String THROWN_EXCEPTIONS_KEY = "THROWN_EXCEPTION_KEY"; @SuppressWarnings("unchecked") private static Set<Exception> getThrown(ExtensionContext context) { ExtensionContext cOntainerContext= getAncestorContainerContext(context) .orElseThrow(IllegalStateException::new); retrun (Set<Exception>) containerContext .getStore(NAMESPACE) .getOrComputeIfAbsent( THROWN_EXCEPTIONS_KEY, ignoredKey -> new HashSet<>()); } private static Optional<ExtensionContext> getAncestorContainerContext( ExtensionContext context) { Optional<ExtensionContext> cOntainerContext= Optional.of(context); while (containerContext.isPresent()) && !(containerContext.get() instanceof ContainerExtenstionContext)) cOntainerContext= containerContext.get().getParent(); return containerContext; } 

    现在存储一个异常就非常简单了:

    @Override public void handleException(TestExtensionContext context, Throwable throwable) throws Throwable { if (throwable instanceof Exception) { getThrown(context).add((Exception) throwable); throw throwable; 

    有意思的是,这个扩展还是自扩展的,没准还可以用来做数据统计呢「译者注:这句不太理解,原文 This is actually an interesting extension of its own. Maybe it could be used for analytics as well.」。不管怎样,我们需要一个 public 方法来拿到已抛出的异常列表:

    public static Stream<Exception> getThrownExceptions( ExtensionContext context) { return getThrown(context).stream(); } 

    有了这个方法,其他的扩展就可以检查至今为止所抛出的异常列表了。

    禁用测试

    禁用测试的部分与前节所述十分类似,我们可以很快写出代码:

    @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DisabledIfTestFailedCondition.class) public @interface DisabledIfTestFailedWith { Class <? extends Exception>[] value() default {}; } 

    注意,现在仅允许该注解被用在测试方法上。应用在测试类上也说得过去,不过我们现在先不把它复杂化。因此我们只需要实现接口TestExecutionCondition即可。我们先检查注解是否存在,若是,再拿到用户提供的异常类作为参数,调用 disableIfExceptionWasThrown

    private ConditionEvaluationResult disableIfExceptionWasThrown( TestExtensionContext context, Class<? extends Exception>[] exceptions) { return Arrays.stream(exceptions) .filter(ex -> wasThrown(context, ex)) .findAny(). .map(thrown -> ConditionEvaluationResult.disabled( thrown.getSimpleName() + "was thrown.")) .orElseGet(() -> ConditionEvaluationResult.enabled("")); } private static boolean wasThrown( TestExtensionContext context, Class<? extends Exception> exception) { return CollectExceptionExtension.getThrownExceptions(context) .map(Object::getClass) .anyMatch(exception::isAssignableFrom); } 

    集成

    至此为止需求完成。现在我们可以使用这个注解,在某个特定类型的异常抛出时禁用测试了:

    @CollectExceptions class DisabledIfFailsTest { private static boolean failedFirst = false; @Test void throwException() { System.out.println("I failed!"); failedFirst = true; throw new RuntimeException(); } @Test @DisabledIfTestFailedWith(RuntimeException.class) void disableIfOtherFailedFirst() { System.out.println("Nobody failed yet! (Right?)"); assertFalse(failedirst); } } 

    回顾总结

    哇哦,本篇的代码还挺多的!不过相信到此你已经能完全理解怎么在 JUnit 5 中实现条件测试了:

    • 创建一个注解,并使用 @ExtendWith 注解它,然后提供你自己实现的条件类
    • 实现 ContainerExecutionCondition 或 /和 TestExecutionCondition
    • 检查测试类上是否应用了你新创建的注解
    • 检查特定条件是否实现,并返回结果

    除此以外,我们还看到注解之间可以组合,学到如何使用 JUnit 提供的 store 来保存数据,以及一个扩展的实现,如何通过自定义注解的加入变得更加优雅。

    更多关于 ~~旗帜~~ 扩展点的故事「译者注:原文为 more fun with ~~flag~~ extension points , more fun with flags 是生活大爆炸中谢耳朵讲国旗的故事一部」,请参考下篇文章,我们会探讨关于参数注入的问题。


    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2092 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 229ms UTC 00:32 PVG 08:32 LAX 16:32 JFK 19:32
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86