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

「译」JUnit 5 系列:基础入门

阅读更多

原文地址:http://blog.codefx.org/libraries/junit-5-basics/
原文日期:25, Feb, 2016
译文首发:Linesh 的博客:JUnit 5 系列:基础入门
我的 Github:http://github.com/linesh-simplicity

上周我们刚刚搭建好了 JUnit 5 的环境,现在我们可以写测试了。这节就让我们来写它几个吧!

概述

本文章是这个 JUnit 5 系列的一部分:

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

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

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

目录

  • 设计哲学
  • JUnit 5预备:
    • 包可见性
    • 测试的生命周期
      • @Test
      • Before 和 After
      • 禁用测试
      • 测试类的生命周期
    • 断言
      • 常规断言
      • 扩展断言
    • 假言/判定(Assumptions)
    • 测试嵌套
    • 测试命名
  • 回顾
  • 分享&关注

设计哲学

新的架构设计(这个我们日后聊),其关注点在高扩展性。如果后面出现了什么神之测试技术(至少对我们广大 Java 汪来说很神的),它们也可能在 JUnit 5 的架构下被实现。

不过当前来说,涉及的基础知识与 JUnit 4 是非常相似的。JUnit 5 的改动并不激进,相反它的优化历程是小心翼翼,小步迭代的。因此,开发者应该会对新的 API 感到非常熟悉。至少我是这样的,我相信你也不会感觉陌生:

class Lifecycle {
 
    @BeforeAll
    staticvoidinitializeExternalResources() {
        System.out.println("Initializing external resources...");
    }
 
    @BeforeEach
    voidinitializeMockObjects() {
        System.out.println("Initializing mock objects...");
    }
 
    @Test
    voidsomeTest() {
        System.out.println("Running some test...");
        assertTrue(true);
    }
 
    @Test
    voidotherTest() {
        assumeTrue(true);
 
        System.out.println("Running another test...");
        assertNotEquals(1, 42, "Why wouldn't these be the same?");
    }
 
    @Test
    @Disabled
    voiddisabledTest() {
        System.exit(1);
    }
 
    @AfterEach
    voidtearDown() {
        System.out.println("Tearing down...");
    }
 
    @AfterAll
    staticvoidfreeExternalResources() {
        System.out.println("Freeing external resources...");
    }
 
}

是吧?这里并没有很大的改动。

JUnit 5 预备

包可见性

JUnit 5 最明显的变化应该是,不再需要手动将测试类与测试方法为 public 了。包可见的访问级别就足够了。当然,私有(private)访问还是不行的。我认为这个变化是合理的,也符合我们对可见性的一般直觉。

这很好!至少可以少打几个字母了。不过,我相信你也不是每次都手打这几个字母的,是吧?尽管如此还是很好,少一些关键字,你在看测试的时候也少些切换。

测试的生命周期

@Test

JUnit 中最基本的注解非 @Test 莫属了。它会标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。

它的 API 和作用并没有变化,不过它不再接受任何参数了。若要测试是否抛出异常,你可以通过新的断言 API来做到;不过就我所知,目前还没有超时选项timeout的替代品。

与 JUnit 4一样,JUnit 5 会为每个测试方法创建一个新的实例。

Before 和 After

你可能需要执行一些代码来在测试执行前后完成一些初始化或销毁的操作。在 JUnit 5 中,有4个注解你可能会用于如此工作:

@BeforeAll
只执行一次,执行时机是在所有测试和 @BeforeEach 注解方法之前。

@BeforeEach
在每个测试执行之前执行。

@AfterEach
在每个测试执行之后执行。

@AfterAll
只执行一次,执行时机是在所有测试和 @AfterEach 注解方法之后。

因为框架会为每个测试创建一个单独的实例,在 @BeforeAll/@AfterAll 方法执行时尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法。

注解了同样一个注解的不同方法,其执行次序是不可预知的,包括对继承来的方法也适用。这是开发团队经过审慎思考后的决定,即把单元测试与集成测试的关注点分开。集成测试可能需要方法间更紧密的协作,但一个单元测试不应该对其他的单元测试有所依赖。而对于集成测试——也叫场景测试——的支持,也已在团队的计划中

除了名字有所不同,这几个注解与 JUnit 4 中的注解工作方式完全一样。无独有偶跟主流意见一致,我也觉得这个新的命名不能说服我其必要性。这个 issue 下有更多的讨论。

禁用测试

今儿星期五,抬头一看已经4点半,无心工作的你想回家了?完全理解,在测试上怒拍一个 @Disabled 注解即可。有良心的话写个忽略测试的理由是极好的,不过也可以不带此参数。

@Test 
@Disabled("你丫就是存心跑不过的是不?!")
voidfailingTest() {
    assertTrue(false);
}

测试类的生命周期

JUnit 团队发布的第一版原型中,包含了一个对 测试类的生命周期 的描述,有意思的是,这个特性在 alpha 版本的发布中未被加入。这个生命周期模型建议,在被测类的多个测试方法中使用一个同样的实例,因为这样我们就可以通过改变对象的状态,进而实现在多个测试方法中的交互。(我也再说一遍,这更像是 场景测试 要管的事。)

正如我在第一版公测时所说,这样的特性99%的场景下是有害的,只有另外1%的场合下才有真正的用处。我只能说,还好这个特性被摒弃了。想想你的单元测试,如果它们必须靠在方法间维护状态来工作,这画面简直太美我不敢看。

断言

如果说 @Test@Before...@After... 等注解是一个测试套件的骨架,那么断言就是它的心脏。准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果。否则,就说明当前测试失败了。

常规断言

一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。无论哪种检查,断言方法都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来。


@Test
voidassertWithBoolean() {
    assertTrue(true);
    assertTrue(this::truism);
 
    assertFalse(false, () -> "Really " + "expensive " + "message" + ".");
}
 
booleantruism() {
    return true;
}
 
@Test
voidassertWithComparison() {
    List<String> expected = asList("element");
    List<String> actual = new LinkedList<>(expected);
 
    assertEquals(expected, actual);
    assertEquals(expected, actual, "Should be equal.");
    assertEquals(expected, actual, () -> "Should " + "be " + "equal.");
    
    assertNotSame(expected, actual, "Obviously not the same instance.");
}

如你所见,JUnit 5 的 API 并无太多变化。断言方法的命名是一样的,方法同样接受两个参数,分别是一个期望值与一个实际值。

期望值与实际值的传入顺序非常重要,无论是对于理解测试的内容,还是理解失败时的错误信息,但有时还是很容易弄错,这点很坑。不过仔细想想,也没什么更好的办法,除非你自己创建一个新的断言框架。既然市面上已有对应的产品如 Hamcrest (ugh!) 和 AssertJ (yeah!译者表示:不太清楚这欢呼的梗在哪里)等,再浪费有限的时间去造轮子明显不值得。毕竟最重要的是保证你的断言库专注于一件事,借鉴已有实现可以节省成本。

哦对了,失败信息现在是作为最后传入的参数了。我很喜欢这个细节,因为,它让你专注于真正重要之事——那两个需被断言的值。由于拥抱了 Java 8 的缘故,真值断言方法现在也接受 supplier 参数了,又是一个暖心的小细节。

扩展断言

除了那种一般的检查特定实例或属性的断言外,还有一些其他类型的断言。

这里要讲的第一个甚至都不是个真正的断言,它做的事就是强行让测试失败,并提供一个失败信息。

@Test
voidfailTheTest() {
    fail("epicly");
}

还有 assertAll 方法,它接受可变数量的断言作为参数,并保证它们全部得到执行,然后再把错误信息(如果有)一并汇报出来。

@Test
voidassertAllProperties() {
    Address address = new Address("New City", "Some Street", "No");
 
    assertAll("address",
            () -> assertEquals("Neustadt", address.city),
            () -> assertEquals("Irgendeinestraße", address.street),
            () -> assertEquals("Nr", address.number)
    );
}
org.opentest4j.MultipleFailuresError: address (3 failures)
    expected: <Neustadt> but was: <New City>
    expected: <Irgendeinestraße> but was: <Some Street>
    expected: <Nr> but was: <No>

这个特性在检查对象的多个属性值时非常有用。按照一般的做法,测试在第一个断言失败时就会挂掉了,此时只有第一个出错的地方得到提示,而你无法得知其他值的断言是否成功,只好再跑一遍测试。

最后,我们终于有了 assertThrows 和 expectThrows 方法。两者均会在被测方法未抛出预期异常时失败。而后者还会返回抛出的异常实例,以用于后续的验证,比如,断言异常信息包含正确的信息等。

@Test
voidassertExceptions() {
    assertThrows(Exception.class, this::throwing);
 
    Exception exception = expectThrows(Exception.class, this::throwing);
    assertEquals("Because I can!", exception.getMessage());
}

假言/判定(Assumptions)


假言/判定允许你仅在特定条件满足时才运行测试。这个特性能够减少测试组件的运行时间和代码重复,特别是在假言都不满足的情况下。


@Test
voidexitIfFalseIsTrue() {
    assumeTrue(false);
    System.exit(1);
}
 
@Test
voidexitIfTrueIsFalse() {
    assumeFalse(this::truism);
    System.exit(1);
}
 
privatebooleantruism() {
    return true;
}
 
@Test
voidexitIfNullEqualsString() {
    assumingThat(
            "null".equals(null),
            () -> System.exit(1)
    );
}

假言/判定适用于两种情形,要么是你希望在某些条件不满足时中止测试,要么是你希望仅当某个条件满足时才执行(部分)测试。主要的区别是,被中止的测试是以被禁用(disabled)的形式被报告,此时没有测试任何内容,因为条件得不到满足。

测试嵌套

在 JUnit 5 中,嵌套测试几乎不费吹灰之力。你只需要在嵌套的类上添加 @Nested 注解,类中的所有方法即会被引擎执行:


package org.codefx.demo.junit5;
 
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
class Nest {
    
    int count = Integer.MIN_VALUE;
    
    @BeforeEach
    voidsetCountToZero() {
        count = 0;
    }
    
    @Test
    voidcountIsZero() {
        assertEquals(0, count);
    }
    
    @Nested
    class CountGreaterZero {
 
        @BeforeEach
        voidincreaseCount() {
            count++;
        }
 
        @Test
        voidcountIsGreaterZero() {
            assertTrue(count > 0);
        }
 
        @Nested
        class CountMuchGreaterZero {
 
            @BeforeEach
            voidincreaseCount() {
                count += Integer.MAX_VALUE / 2;
            }
 
            @Test
            voidcountIsLarge() {
                assertTrue(count > Integer.MAX_VALUE / 2);
            }
 
        }
 
    }
    
}

如你所见,嵌套类中的 @BeforeEach(及 @AfterEach )注解也工作良好。不过,构造顺序似乎还未被写入文档,它们的初始化次序是从外向内的。这也让你能叠加式地为内部类准备测试数据。

如果嵌套的内部测试想要存取外部测试类的字段,那么嵌套类本身不应该是静态的。但这样一来也就禁止了静态方法的使用,因而这种场景下@BeforeAll 和 @AfterAll 方法也就无法使用了(还是说终有他法实现?

你可能有疑惑,嵌套的内部测试类有什么用。个人而言,我用内部类来渐进测试接口,其他人则多用于保持测试类短小专注。后者同时也有一个经典的例子来说明,例子由 JUnit 团队提供,它测试了一个栈:

class TestingAStack {
 
    Stack<Object> stack;
    boolean isRun = false;
 
    @Test
    voidisInstantiatedWithNew() {
        new Stack<Object>();
    }
 
    @Nested
    class WhenNew {
 
        @BeforeEach
        voidinit() {
            stack = new Stack<Object>();
        }
 
        // some tests on 'stack', which is empty
 
        @Nested
        class AfterPushing {
 
            String anElement = "an element";
 
            @BeforeEach
            voidinit() {
                stack.push(anElement);
            }
 
            // some tests on 'stack', which has one element...
 
        }
    }
}

在上面的例子中,栈的状态改变会反映到内层的测试类中,其中内部类又基于自身的场景执行了一些测试。

测试命名

JUnit 5 提供了一个注解 @DisplayName,它用以为开发者提供更可读的测试类和测试方法信息。

上面的 stack 测试例子加上该注解以后就变成这样:

@DisplayName("A stack")
class TestingAStack {
 
    @Test
    @DisplayName("is instantiated with new Stack()")
    voidisInstantiatedWithNew() { /*...*/ }
 
    @Nested
    @DisplayName("when new")
    class WhenNew {
 
        @Test
        @DisplayName("is empty")
        voidisEmpty() { /*...*/ }
 
        @Test
        @DisplayName("throws EmptyStackException when popped")
        voidthrowsExceptionWhenPopped() { /*...*/ }
 
        @Test
        @DisplayName("throws EmptyStackException when peeked")
        voidthrowsExceptionWhenPeeked() { /*...*/ }
 
        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
 
            @Test
            @DisplayName("it is no longer empty")
            voidisEmpty() { /*...*/ }
 
            @Test
            @DisplayName("returns the element when popped and is empty")
            voidreturnElementWhenPopped() { /*...*/ }
 
            @Test
            @DisplayName(
                    "returns the element when peeked but remains not empty")
            voidreturnElementWhenPeeked(){ /*...*/ }
        }
    }
}

这是一份TDDer 看了会感动,BDDer 看了会流泪的测试结果输出。

回顾

差不多就这些了,恭喜你终于读完了。我们匆匆过完了 JUnit 5 的基本特性,现在,你应该了解了所有写测试的必备知识了:包括如何为方法添加生命周期注解(@[Before|After][All|Each]、如何注解测试方法本身(@Test)、如何嵌套测试(@Nested)、如何给测试一个好信息(@DisplayName),你也应该能了解断言和假言判定是如何工作的了(基本上与前版无异)。

不过这可还没完!我们还没聊到 测试方法的条件执行,没聊到非常酷的 参数注入 ,以及 JUnit 5 的扩展机制 和 架构体系 呢。放心,这真的是最后了,这些话题我们会一个月后再聊,现在你可以先休息一下啦。

敬请期待下集!

0
2
分享到:
评论

相关推荐

    Junit4.12和依赖包

    JUnit是Java编程语言中最常用的单元测试框架之一,它极大地简化了对代码进行验证和调试的过程。Junit4.12是该框架的一个稳定版本,它包含了许多改进和新特性,使得测试更加灵活和强大。在本文中,我们将深入探讨...

    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-4.12.rar包及依赖包

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

    junit-libs:junit测试包

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

    Junit5依赖整合包

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

    junit5学习入门

    ### JUnit5学习入门知识点详解 #### 一、JUnit简介 JUnit是一个开源的Java单元测试框架,被广泛应用于软件开发中的测试驱动开发(TDD)流程之中。它支持开发者通过编写测试代码来验证软件功能是否符合预期,从而提高...

    junit-platform-launcher-1.8.0-M1-API文档-中文版.zip

    赠送jar包:junit-platform-launcher-1.8.0-M1.jar; 赠送原API文档:junit-platform-launcher-1.8.0-M1-javadoc.jar; 赠送源代码:junit-platform-launcher-1.8.0-M1-sources.jar; 赠送Maven依赖信息文件:junit-...

    junit单元测试

    JUnit Platform是一个运行测试的基础架构,支持多种测试框架;JUnit Jupiter是JUnit 5的主要实现,包含了新的API和注解,如`@Test`, `@DisplayName`, `@ParameterizedTest`, `@BeforeEach`, `@AfterEach`等;JUnit ...

    junit-jupiter-engine-5.8.2-API文档-中文版.zip

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

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

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

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

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

    junit-jupiter-api-5.8.2-API文档-中文版.zip

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

    junit-4.11.jar

    这个版本在前一版本的基础上进行了一系列的改进和增强,使得测试工作更加高效且易用。当我们下载并解压"junit-4.11.jar"文件后,我们得到了一个可以直接运行的Java库,它包含了JUnit 4.11的所有功能。 JUnit 4.11的...

    探索JUnit4扩展:深入Rule

    《探索JUnit4扩展:深入Rule》 JUnit是Java开发者最常用的单元测试框架,它极大地简化了测试代码的编写。在JUnit4中,引入了一个强大的特性——Rule,这使得测试更加灵活且可定制化。本文将深入探讨Rule的概念、...

    junit-4.13.1.jar

    JUnit 是一个 Java 编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。

    Junit5.zip

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

    junit5.jar

    《深入理解JUnit 5:Java单元测试框架的革新》 JUnit 5,作为Java开发者最常用的单元测试框架,是JUnit系列的最新版本,为Java测试带来了显著的改进和创新。这个名为"junit5.jar"的文件正是JUnit 5的核心库,它包含...

    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 ```...

    junit-vintage-engine-5.6.2.jar_junit testng

    junit-vintage-engine-5.6.2.jarjunit-vintage-engine-5.6.2.jarjunit-vintage-engine-5.6.2.jar

    junit5-r5.5.2.zip

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

Global site tag (gtag.js) - Google Analytics