`
sodagreen.simplicity
  • 浏览: 22533 次
文章分类
社区版块
存档分类

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

阅读更多

原文地址: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)
voiddoesNotRunOnWindows() {
    assertTrue(false);
}

棒。

更简洁的API

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

@TestExceptOnOs(OS.WINDOWS)
voiddoesNotRunOnWindowsEither() {
    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)
voiddoesNotRunOnWindoesEither() {
    assertTrue(false);
}

还挺好看的,是不?

<iframe id="iframe_0.5536054613111516" style="margin: 0px; padding: 0px; border-width: initial; border-style: none; width: 935px;" src="https://www.cnblogs.com/show-blocking-image.aspx?url=http%3A%2F%2F7xqu8w.com1.z0.glb.clouddn.com%2Fjunit-5-conditions.jpg&amp;maxWidth=935&amp;origin=http://www.cnblogs.com&amp;iframeId=iframe_0.5536054613111516" frameborder="0" scrolling="no" height="406"></iframe>

「译者注:英文中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      *       * [ ... ]     */
    voidhandleTestExecutionException(
        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")
privatestatic 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<>());
}

privatestatic 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
publicvoidhandleException(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方法来拿到已抛出的异常列表:

publicstatic 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(""));
}

privatestaticbooleanwasThrown(
        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
    voidthrowException() {
        System.out.println("I failed!");
        failedFirst = true;
        throw new RuntimeException();
    }
    
    @Test
    @DisabledIfTestFailedWith(RuntimeException.class)
    voiddisableIfOtherFailedFirst() {
        System.out.println("Nobody failed yet! (Right?)");
        assertFalse(failedFirst);
    }
    
}

回顾总结

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

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

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

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

0
0
分享到:
评论

相关推荐

    junit-libs:junit测试包

    JUnit测试框架由Ernst Leiss和Kent Beck在1997年发起,其后不断演进,目前主要由JUnit团队维护,最新的版本是JUnit 5,它分为JUnit Jupiter、JUnit Platform和JUnit Vintage三个部分。JUnit 5对旧版本进行了重大改进...

    Junit4.12和依赖包

    另外,Junit4.12还引入了`Assume`类,它提供了假设方法,可以在测试开始前检查特定条件。如果条件不满足,测试会跳过而不是失败,这对于处理依赖外部环境的测试很有帮助。 在实际项目中,由于国内访问Junit官网可能...

    junit-4.12.rar包及依赖包

    本文将深入探讨关于"junit-4.12.rar"包及其依赖包,以及如何解决在使用JUnit 4进行单元测试时遇到的"method initializationerror not found"错误。 首先,我们来了解JUnit 4.12版本。这是JUnit的一个稳定版本,发布...

    junit-jupiter-api-5.4.2-API文档-中英对照版.zip

    赠送jar包:junit-jupiter-api-5.4.2.jar; 赠送原API文档:junit-jupiter-api-5.4.2-javadoc.jar; 赠送源代码:junit-jupiter-api-5.4.2-sources.jar; 赠送Maven依赖信息文件:junit-jupiter-api-5.4.2.pom; ...

    junit单元测试

    使用JUnit 5,开发者可以编写更加灵活和可读的测试代码,例如通过参数化测试来运行同一测试用例的不同数据组合,或者使用条件注解来控制测试执行的条件。另外,JUnit 5还引入了Lambda表达式和方法引用,使得测试代码...

    Junit5依赖整合包

    在导入这个"Junit5依赖整合包"后,你将可以使用Junit5的一系列新特性,例如: 1. **Lambda表达式支持**:Junit5允许使用Lambda表达式来编写简洁的测试方法,例如`assertThat(() -&gt; someCode()).isTrue();`。 2. **...

    JUnit技巧:程序员测试实用方法

    2. **断言**:JUnit提供了一系列的断言方法,如`assertEquals()`、`assertTrue()`、`assertFalse()`等,用于比较预期结果与实际结果,当预期不一致时,测试将失败。 3. **注解驱动测试**:从JUnit 4开始,测试用例...

    android-junit5,使用junit 5进行android测试。.zip

    JUnit5是JUnit系列的最新版本,它由三个主要组件组成:JUnit Platform(平台)、JUnit Jupiter(核心引擎)和JUnit Vintage(兼容旧版)。JUnit Platform提供了一个通用的运行器,可以启动测试框架,而JUnit Jupiter...

    JUnit5所需的jar包,导入完就可以用

    在Maven的pom.xml文件中,可以添加如下依赖来引入JUnit5: ```xml &lt;groupId&gt;org.junit.jupiter &lt;artifactId&gt;junit-jupiter-engine &lt;version&gt;5.x.y&lt;/version&gt; &lt;!-- 替换为最新版本 --&gt; &lt;scope&gt;test ```...

    Junit5.jar包,代码测试工具

    - **条件测试**:`@Disabled`注解可以禁用某个测试,`@Tag`则可以标记测试,以便按需选择执行。 2. **Lambda表达式与测试生命周期**: - JUnit5支持Java 8的Lambda表达式,使得在测试初始化和清理过程中编写简洁...

    junit5.jar

    同时,JUnit Jupiter还提供了参数化测试、条件测试、生命周期方法等功能,满足了复杂测试场景的需求。 JUnit Vintage则保留了对JUnit 4测试的支持,这意味着在JUnit 5环境中可以无缝运行JUnit 4编写的测试,确保了...

    Junit5.zip

    JUnit5是Java编程语言中最流行的单元测试框架之一,它的最新版本带来了许多改进和新特性,使得测试更加高效和灵活。本资源包含的`junit5.jar`是JUnit5的运行库,可以用于运行使用JUnit5编写的测试用例。而`junit5-...

    junit-4.11.jar

    5. **分类(Categories)**:新增了测试分类功能,开发者可以将测试分为不同的类别,便于组织和执行特定的测试集。 6. **改进的错误信息**:当测试失败时,JUnit 4.11提供更详细的错误信息,有助于快速定位问题。 ...

    Junit测试案例使用

    5. Junit 测试的分类:Junit 有不同的使用技巧,以后慢慢地分别讲叙,如 Class 测试、Jsp 测试、Servlet 测试、Ejb 测试等。 6. Junit 的下载和安装:去 Junit 主页下载最新版本 3.8.1 程序包 junit-3.8.1.zip,用 ...

    junit5-r5.5.2.zip

    《深入理解JUnit 5:基于r5.5.2版本》 JUnit,作为Java领域最常用的单元测试框架,自诞生以来就备受开发者喜爱。本文将深入解析JUnit 5的r5.5.2版本,帮助读者全面掌握其核心功能与用法。 1. **JUnit 5简介** ...

    使用Junit4.12需要用的两个包,官网在国内无法下载

    首先,JUnit 4.12是JUnit系列的一个版本,发布于2013年,提供了丰富的断言方法、测试注解和参数化测试等功能,极大地简化了单元测试的编写。在使用JUnit 4.12时,通常需要两个核心的jar包:junit.jar和hamcrest-core...

    实验三:junit测试.rar

    JUnit 5是当前的最新版本,相比之前的版本,它引入了许多新特性,如参数化测试、条件测试、测试注解的增强等。 1. **安装与引入JUnit** 在Java项目中使用JUnit,首先需要将其作为项目的依赖引入。如果你使用的是...

    junit-4.11 jar包、源文件、操作文档

    JUnit是Java编程语言中最常用的单元测试框架之一,主要用于编写和执行可重复的、自动化控制的测试用例。这个"junit-4.11"版本是JUnit的一个重要里程碑,它包含了许多新特性和改进,旨在提升开发者的测试体验。在这个...

    探索JUnit4扩展:深入Rule

    JUnit是Java开发者最常用的单元测试框架,它极大地简化了测试代码的编写。在JUnit4中,引入了一个强大的特性——Rule,这使得测试更加灵活且可定制化。本文将深入探讨Rule的概念、用法以及如何通过Rule实现自定义的...

Global site tag (gtag.js) - Google Analytics