构建高质量软件:持续集成与持续交付系统实践
上QQ阅读APP看书,第一时间看更新

2.1 Hamcrest概述

Hamcrest是一个框架,可用于创建匹配器对象,这些匹配器对象常用于编写特定条件下的规则匹配。Hamcrest通常用于与一些测试工具(比如JUnit、Mockito、Powermock、JMock、REST-assured等)进行集成。除了应用于单元测试之外,Hamcrest还可以直接应用于其他场景,比如数据验证、逻辑判断等。Hamcrest的官方地址为http://hamcrest.org/

由于Hamcrest具有灵活性、可扩展性,以及强大的规则匹配能力,因此很多开源项目都引入并使用它,通过Maven中央仓库对Hamcrest的引用统计可以看出,当前已有4281个知名开源项目引用Hamcrest(如图2-1所示),其中不仅包括一些测试工具,还不乏一些大型的分布式计算平台(比如Spark、Hadoop等),也就是说,Hamcrest不仅可以应用于测试代码的期望结果与实际结果断言中,而且可以直接应用于软件源代码中。

037-01

图2-1 开源项目大规模引用Hamcrest

在大致了解了Hamcrest是干什么的,及其得到大规模引用之外,我们需要客观对比一下,相较于直接使用JUnit断言方法,Hamcrest具备哪些优势。在正式使用Hamcrest之前,我们需要将其引入Maven工程之中。JUnit 4.4以后的版本默认依赖Hamcrest,如下所示,JUnit 4.13依赖于hamcrest-core的1.3版本。

C:\Users\wangwenjun\IdeaProjects\cicd>mvn dependency:tree -Dverbose
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------------------------------------------------------
[INFO] Building cicd 1.0-SNAPSHOT
[INFO] -----------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ cicd ---
[INFO] com.wangwenjun.books:cicd:jar:1.0-SNAPSHOT
[INFO] +- junit:junit:jar:4.13:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] -----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -----------------------------------------------------------------------
[INFO] Total time: 3.598 s
[INFO] Finished at: 2020-07-13T22:16:05+08:00
[INFO] Final Memory: 13M/193M
[INFO] -----------------------------------------------------------------------

相对于本节所学的内容,hamcrest-core 1.3版本有些老旧,因此最好显式指定一个较新的Hamcrest版本,否则本章中的很多例子都将无法执行,如下所示的pom配置是笔者写作本书时所用的配置,在此列出以供大家参考借鉴。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>2.2</version>
    <scope>test</scope>
</dependency>

环境准备就绪,我们将从如下几个方面对比JUnit断言方法与Hamcrest对象匹配器的使用,以及相较于JUnit断言方法,Hamcrest到底具备哪些优点。

(1)可读性强

相较于JUnit提供的断言方法,assertThat具有更好的可读性,这主要得益于Hamcrest各种对象匹配器的陈述式编程风格(关于陈述式编程风格的详情,大家可以参考维基百科的介绍资料https://en.wikipedia.org/wiki/Declarative_programming),下面我们来看一个简单的例子,如程序代码2-1所示。

程序代码2-1 Hamcrest具有更好的可读性

import org.junit.Assert;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;

public class JunitAssertionVsHamcrest
{

    @Test
    public void hamcrestMoreReadability()
    {
        //JUnit的断言方法,期望值为10,实际值为4+6的计算结果,该方法用于断言期望值与实际值是否相等。
        Assert.assertEquals(10, 4 + 6);
        //assertThat方法,实际值(4+6的计算结果)是否与10相等。
        assertThat(4 + 6, is(equalTo(10)));

        //JUnit的断言方法,期望值为10,实际值为4+7的计算结果,该方法用于断言期望值与实际值是否不相等。
        Assert.assertNotEquals(10, 4 + 7);
        //assertThat方法,实际值(4+7的计算结果)是否与10不相等。
        assertThat(4 + 7, is(not(equalTo(10))));
    }
}

请注意,JUnit提供的断言方法的期望值是第一个参数,而实际值则是第二个参数。assertThat则与之相反,第一个参数是实际值,第二个参数是Matcher(对象匹配器),比如,equalTo方法的返回值实际上是一个Matcher,is方法的返回值实际上也是一个Matcher。对比上面的这两个小例子不难看出,assertThat方法具有更好的可读性,各种Matcher的组合非常接近于我们日常的交流表达方式(这也是陈述式编程风格所倡导的一大亮点)。

(2)良好的错误信息输出

断言失败时,assertThat提供的错误提示更加友好易懂,可以帮助开发人员快速发现和定位问题所在。首先,我们要基于上述代码增加JUnit的套件(suite)方法(如程序代码2-2所示,因为我们要在套件方法中创建一个保存姓名的list容器),然后分别用JUnit断言和assertThat断言输出当某个名字不存在于list容器时的错误信息。

程序代码2-2 增加套件方法

//这里省略部分代码。
private List<String> names;
@Before
public void setUp()
{
    names = Arrays.asList("Alex", "Jeffrey", "Alice", "John", "Jack", "Wangwenjun");
}
//这里省略部分代码。

接下来,我们先基于JUnit断言方法进行单元测试(如程序代码2-3所示),很遗憾,JUnit中并未提供某个元素是否存在于集合之中的断言方法,因此我们只能借助assertTrue工具方法来实现。

程序代码2-3 JUnit断言失败

@Test
public void junitAssertionFailure()
{
    Assert.assertTrue(names.contains("Tina"));
}

很明显,names容器中并没有包含元素“Tina”,因此运行该单元测试方法肯定会失败,不过这也正是我们所期望的。运行该单元测试方法,我们将会看到如图2-2所示的错误信息(根本看不出来发生了什么,如果程序员想要定位问题所在,就要通过测试代码进行排查)。

040-01

图2-2 几乎没有任何帮助的错误信息

为了进行对比,接下来再通过assertThat来断言某个元素存在于list容器中(如程序代码2-4所示),然后观察当断言失败时,程序是否能够提供有助于快速定位问题所在的错误提示信息。

程序代码2-4 assertThat断言失败

@Test
public void assertThatFailure()
{
    assertThat(names, hasItem("Tina"));
}

在assertThat方法中,如果要判断某个元素是否存在于容器中,则只需要使用hasItem方法返回的Matcher即可,而无须像assertTrue那样,必须调用list的contains方法,显式地返回一个布尔结果。运行该单元测试方法,根据输出的错误信息(如图2-3所示),我们可以轻而易举地发现问题所在。

041-01

图2-3 assertThat可以提供非常有帮助的错误信息

除此之外,某些IDE工具(比如,Intellij Idea)还可以对assertThat提供更好的支持,点击图2-3中的“Click to see difference”,还会弹出非常有帮助的错误信息,并且提供了对比功能(如图2-4所示)。

041-02

图2-4 Intellij Idea的失败对比

(3)灵活

虽然目前我们还没有系统地讲解Hamcrest的具体用法,但是想必大家通过之前的代码示例,已经可以感受到Hamcrest的灵活性了。下文将通过多个对象匹配器逻辑组合的示例(如程序代码2-5所示),讲解Hamcrest的灵活性。

程序代码2-5 assertThat的灵活性示例代码

@Test
public void assertThatAny()
{
    //如果anyOf方法中的任何一个Matcher匹配成功,则断言成功。
    //在Hamcrest中,is和equalTo实际上是等价的,is起到的是语法糖的作用。
    assertThat("Hello", anyOf(is("Hello"), equalTo("Hello"),
         containsString("llo"), endsWith("LLO"))
    );
}

@Test
public void assertThatAll()
{
    //allOf方法要求所有的Matcher全部匹配成功才能断言成功,很明显,“hello”并不是以“LLO”结尾的,所以断言会失败。
    assertThat("Hello", allOf(is("Hello"), equalTo("Hello"),
        containsString("llo"), endsWith("LLO"))
    );
}

运行上面的单元测试,会看到assertThatAll单元测试方法提供了非常详细的断言失败错误信息,如图2-5所示。

042-01

图2-5 Hamcrest支持多个匹配器的组合

(4)可移植性强

由于单元测试没有使用JUnit的断言方法(这就意味着没有侵入JUnit的代码),而是直接使用Hamcrest的assertThat方法及各种对象匹配器,因此单元测试代码很容易就能移植到其他单元测试工具或框架中,比如TestNG、Spock等。

(5)可自定义对象匹配器

JUnit的Assert类中定义了很多用于断言的方法(如图2-6所示),这就意味着我们只能使用这些断言方法进行期望值与实际值的断言操作,因为JUnit的Assert类不允许开发者进行扩展。例如,要想断言容器中是否存在某个元素,我们只能通过assertTrue显式调用容器的contains方法的返回值进行断言,而Hamcrest允许开发者根据自己的需要自定义对象匹配器,关于自定义对象匹配器的相关内容将在2.2节详细讲述。

042-02

图2-6 JUnit Assert提供的断言方法