2.2 Hamcrest对象匹配器详解

在了解了Hamcrest及其优点之后,本节将深入掌握Hamcrest所提供的各类灵活且强大的对象匹配器的用法。总体来说,Hamcrest的对象匹配器大致可以分为如下几类。

  • org.hamcrest.beans:对象实例和对象属性相关的匹配器,其底层使用的是Property-Descriptor相关的API。
  • org.hamcrest.collection:Java容器和元素关系相关的匹配器。
  • org.hamcrest.number:Double及BigDecimal相关的匹配器。
  • org.hamcrest.object:Object对象相关的匹配器(由于对象匹配器比较简单,因此本节不做讲解)。
  • org.hamcrest.text:文本字符相关的匹配器。
  • org.hamcrest.xml:XML文档相关的匹配器。
  • org.hamcrest.core:核心匹配器,比如is、not、equalTo、anyOf、allOf等都属于这类匹配器,也是使用最多的对象匹配器。

2.2.1 org.hamcrest.core

核心匹配器是应用范围最广泛的一组匹配器,JUnit 4.4以后的版本默认依赖Hamcrest的核心匹配器。大多数情况下,核心匹配器其实已能足够应对日常工作的需要了。在使用核心匹配器时,建议大家通过静态导入的方式,将所有核心匹配器引入单元测试类中,具体做法如下。

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

核心匹配器相对来说比较多(如图2-7所示),限于篇幅,本节无法讲解所有匹配器的具体用法,因此这里只是挑选介绍几个逻辑相关的匹配器,其他的匹配器将在示例代码中进行介绍,比如,Mockito和Powermock中都会讲解核心匹配器的使用。

043-01

图2-7 Hamcrest核心匹配器列表

1)allOf:只有当所有的匹配条件都满足时,断言才能成功。

@Test
public void testAllOf()
{
    String actually = "Hello Hamcrest";
    //只有当下列匹配条件都满足时,才能断言成功。
    //actually与"Hello Hamcrest"的值相同(equal)。
    assertThat(actually, allOf(is(equalTo("Hello Hamcrest")),
            //actually中包含字符串"llo Ha"。
            containsString("llo Ha"),
            //actually是以"Hello"开头的。
            startsWith("Hello"),
            //actually是以"crest"结尾的。
            endsWith("crest"),
            //actually是String类型的一个实例。
            instanceOf(String.class),
            //actually不为null。
            notNullValue(),
            stringContainsInOrder("Hello", "Hamcrest"))
    );
    //与上面的写法等价,将allOf方法传入匹配器List。
    assertThat(actually, allOf(Arrays.asList(
            //actually与"Hello Hamcrest"的值相同(equal)。
            is(equalTo("Hello Hamcrest")),
            //actually中包含字符串"llo Ha"。
            containsString("llo Ha"),
            //actually是以"Hello"开头的。
            startsWith("Hello"),
            //actually是以"crest"结尾的。
            endsWith("crest"),
            //actually是String类型的一个实例。
            instanceOf(String.class),
            //actually不为null。
            notNullValue(),
            stringContainsInOrder("Hello", "Hamcrest")))
    );

在上述示例代码中,只有当所有的条件匹配都是符合期望的,断言才能够成功。allOf方法提供了两种重载形式,Matcher的可变长数组和Iterable<Matcher>,具体形式如下。

  • allOf(java.lang.Iterable<org.hamcrest.Matcher<? super T>> matchers)
  • allOf(org.hamcrest.Matcher<? super T>... matchers)

2)anyOf:若有任意一个匹配条件成立,则断言成功。

@Test
public void testAnyOf()
{
    String actually = "Hello Hamcrest";
    //下列匹配条件只要有一个满足,则断言成功。
    //actually与"Hello Hamcrest1"的值相同(equal)。 ×
    assertThat(actually, anyOf(is(equalTo("Hello Hamcrest1")),
            //actually中包含字符串"llo Xa"。 ×
            containsString("llo Ha"),
            //actually是以"Hello"开头的。 √
            startsWith("Hello"),
            //actually是以"crest"结尾的。 ×
            endsWith("crest?"),
            //actually是Integer类型的一个实例。 ×
            instanceOf(Integer.class),
            //actually不为null。 ×
            nullValue(),
            //顺序错误。 ×
            stringContainsInOrder("Hamcrest", "Hello"))
    );
    //与上面的写法等价。

        assertThat(actually, anyOf(Arrays.asList(
       //actually与"Hello Hamcrest1"的值相同(equal)。 ×
            is(equalTo("Hello Hamcrest1")),
            //actually中包含字符串"llo Xa"。 ×
            containsString("llo Ha"),
            //actually是以"Hello"开头的。 √
            startsWith("Hello"),
            //actually是以"crest"结尾的。 ×
            endsWith("crest?"),
            //actually是Integer类型的一个实例。 ×
            instanceOf(Integer.class),
            //actually不为null。 ×
            nullValue(),
            //顺序错误。 ×
            stringContainsInOrder("Hamcrest", "Hello")))
    );
}

在上面的代码中,只有“Hello Hamcrest”以“Hello”开头是正确的,其他的条件匹配都不成立,但是这并不妨碍断言的最终成功。anyOf方法提供了两种重载形式,具体如下所示。

  • anyOf(java.lang.Iterable<org.hamcrest.Matcher<? super T>> matchers)
  • anyOf(org.hamcrest.Matcher<? super T>... matchers)

3)both:两个匹配条件的逻辑“与”。

@Test
public void testBoth()
{
    String actually = "Hello Hamcrest";
    assertThat(actually, both(
            allOf(
                    //actually与"Hello Hamcrest"的值相等(equal)。
                    is(equalTo("Hello Hamcrest")),
                    //actually中包含字符串"llo Ha"。
                    containsString("llo Ha"),
                    //actually是以"Hello"开头的。
                    startsWith("Hello"),
                    //actually是以"crest"结尾的。
                    endsWith("crest"))
            ).and(
                allOf(
                    instanceOf(String.class),
                    //actually不为null。
                    notNullValue(),
                    //顺序正确。
                    stringContainsInOrder("Hello", "Hamcrest")
                )
            )
    );
}

上面这段代码稍微有些复杂,但只需要重点关注both().and()语法即可,该语法能够很好地支持both方法和and方法中的对象匹配器,这也是Hamcrest语法的强大之处,不同的Matcher可以实现非常灵活的组合。

4)either:两个匹配条件的逻辑“或”。

@Test
public void testEither()
{
    String actually = "Hello Hamcrest";
    //与"Hello Hamcrest"相等或为null,只要满足一个匹配条件即可断言成功。
    assertThat(actually, either(is(equalTo("Hello Hamcrest"))).or(nullValue()));
}

5)语法糖方法:为了使单元测试方法更具可读性,Hamcrest还提供了很多语法糖方法。比如,equalTo与is(equalTo(...))本身是没有任何区别的,这么做的目的只是为了提高可读性,是一种更具陈述性的表达方式。

2.2.2 org.hamcrest.beans

如果想要判断某个对象O中是否包含属性P、P的值为X,以及两个对象O是否拥有相同的属性,并且每一个属性的值都相等,则可以使用beans下的对象匹配器。比如,使用ORM(Object Relational Mapping,对象关系映射)框架从数据库中获取某个Entity对象,或者调用一个远程方法返回Entity对象时,没有必要在获取对象的每一个实例属性后都进行断言操作,直接通过beans中相关的对象匹配器就可以完成判断。具体实现代码如程序代码2-6所示。

程序代码2-6 org.hamcrest.beans匹配器示例

//这里省略部分代码。
public class SimpleBean
{
    private String name;
    private int age;

    public SimpleBean()
    {
    }

    public SimpleBean(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
//这里省略get和set方法。
}
//下面是单元测试的相关代码,其中包含了beans下匹配器的用法示例。
import org.junit.Test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.samePropertyValuesAs;

public class HamcrestUsageTest
{
    @Test
    public void testHasProperty()
    {
        final SimpleBean bean = new SimpleBean();
        //断言bean中包含属性name。
        assertThat(bean, hasProperty("name"));
    }

    @Test
    public void testHasPropertyWithValue()
    {
        final SimpleBean bean = new SimpleBean("Alex", 35);
        //断言bean中包含属性及期望值。
        assertThat(bean, hasProperty("name", is(equalTo("Alex"))));
        assertThat(bean, hasProperty("age", is(equalTo(35))));
    }

    @Test
    public void testSamePropertyValuesAs()
    {
        final SimpleBean bean1 = new SimpleBean("Alex", 35);
        final SimpleBean bean2 = new SimpleBean("Alex", 35);
        final SimpleBean bean3 = new SimpleBean("Alex", 100);
        //断言bean1和bean2具有相同的属性,并且每个属性值都相等。
        assertThat(bean1, samePropertyValuesAs(bean2));
        //断言bean1和bean3具有相同的属性,并且属性值都相等(忽略对age属性的比较,因为age不相等)。
        assertThat(bean1, samePropertyValuesAs(bean3, "age"));
    }
}

2.2.3 org.hamcrest.collection

collection下的对象匹配器主要用于匹配元素、数组与collection、map之间的关系,下面通过示例代码(关于匹配器的相关信息,代码注释进行了详细描述)进行讲解。

1)IsArray<T>:匹配数组中所有元素的匹配器。

@Test
public void testIsArray()
{
    Integer[] actually = {1, 2, 3};
    //断言匹配actually数组中的元素个数及内容。
    assertThat(actually, is(array(equalTo(1), equalTo(2), equalTo(3))));
    //下面的断言匹配会失败,因为匹配器的顺序与actually 中元素的顺序不一致。
    //assertThat(actually, is(array(equalTo(1), equalTo(3), equalTo(2))));
}

2)IsArrayContaining<T>:匹配数组中是否包含某个元素。

@Test
public void testHasItemInArray()
{
    String[] actually = {"foo", "bar"};
    //断言匹配actually数组中包含元素“foo”。
    assertThat(actually, hasItemInArray(is("foo")));
    //断言匹配actually数组中包含以“ba”开头的元素。
    assertThat(actually, hasItemInArray(startsWith("ba")));
}

3)IsArrayWithSize<E>:匹配数组长度或数组为空。

@Test
public void testIsArrayWithSize()
{
    Integer[] actually = {1, 2, 3};
    //以下三种写法是等价的,都是用于断言匹配actually数组的长度。
    assertThat(actually, arrayWithSize(3));
    assertThat(actually, arrayWithSize(is(3)));
    assertThat(actually, arrayWithSize(equalTo(3)));

    //数组不为空。
    assertThat(actually, is(not(emptyArray())));
}

4)IsArrayContainingInOrder<E>:按顺序匹配数组中的所有元素。

@Test
public void testIsArrayContainingInOrder()
{
    Integer[] actually = {1, 2, 3};
    //断言匹配actually包含元素1、2、3(顺序要求与actually一致),以下三种写法是等价的。
    assertThat(actually, arrayContaining(1, 2, 3));
    assertThat(actually, arrayContaining(equalTo(1), equalTo(2), equalTo(3)));
    assertThat(actually,
    arrayContaining(
        Arrays.asList(equalTo(1), equalTo(2), equalTo(3)))
    );
}

5)IsArrayContainingInAnyOrder<E>:以任意顺序匹配数组中的所有元素。

@Test
public void testArrayContainingInAnyOrder()
{
    Integer[] actually = {1, 2, 3};
    //断言匹配actually是否包含元素1、2、3(允许任意顺序),以下三种写法是等价的。
    assertThat(actually, arrayContainingInAnyOrder(
        equalTo(1), equalTo(3), equalTo(2)));
    assertThat(actually, arrayContainingInAnyOrder(1, 3, 2));
    assertThat(actually, arrayContainingInAnyOrder(
        Arrays.asList(equalTo(1), equalTo(3), equalTo(2)))
    );
}

6)IsCollectionWithSize<E>:断言collection元素的个数。

@Test
public void testIsCollectionWithSize()
{
    Collection<Integer> actually = Arrays.asList(1, 2, 3);
    //断言匹配actually元素的个数,以下两种写法是等价的。
    assertThat(actually, hasSize(3));
    assertThat(actually, hasSize(equalTo(3)));
}

7)IsEmptyCollection<E>:断言collection为空的匹配器。

@Test
public void testIsEmptyCollection()
{
    Collection<Integer> actually = Collections.emptyList();
    //actually为空。
    assertThat(actually, empty());
    //actually为空,且actually中的元素类型为Integer。
    assertThat(actually, emptyCollectionOf(Integer.class));
}

8)IsEmptyIterable<E>:断言Iterable为空的匹配器。

@Test
public void testIsEmptyIterable()
{
    //Collection是Iterable的子接口。
    Collection<Integer> actually = Collections.emptyList();
    //actually为空。
    assertThat(actually, emptyIterable());
    //actually为空,且actually中的元素类型为Integer。
    assertThat(actually, emptyIterableOf(Integer.class));
}

9)IsMapContaining<K,V>:匹配Map中key、value、entry等相关的匹配器。

@Test
public void testIsMapContaining()
{
    //actually map
    Map<String, String> actually = new HashMap<String, String>()
    {
        {
            put("Alex", "Hello Alex");
            put("Wang", "Hello Wang");
            put("Tina", "Hello Tina");
        }
    };

    //断言匹配actually中存在key为“Alex”、value为“Hello Alex”的Entry。
    assertThat(actually, hasEntry("Alex", "Hello Alex"));
    assertThat(actually, hasEntry(is("Alex"), endsWith("Alex")));

    //断言匹配actually中存在Key为“Wang”的item。
    assertThat(actually, hasKey("Wang"));
    assertThat(actually, hasKey(is(equalTo("Wang"))));

    //断言匹配actually中存在value为“Hello Alex”的item。
    assertThat(actually, hasValue("Hello Alex"));
    assertThat(actually, hasValue(is("Hello Alex")));
}

10)IsIn<T>:用于匹配某元素存在于数组(Array)、collection或可变长数组中的对象匹配器。

@Test
public void testIsIn()
{
    //断言匹配ArrayList中存在元素1,但是这种写法已被标注为过期,请使用下面的写法。
    assertThat(1, isIn(Arrays.asList(1, 2, 3)));

    //等价于上一行代码,但未被标注为过期。
    assertThat(1, is(in(Arrays.asList(1, 2, 3))));

    //断言匹配数组中存在元素1,但是这种写法已被标注为过期,请使用下面的写法。
    assertThat(1, isIn(new Integer[]{1, 2, 3}));
    //等价于上一行代码,但未被标注为过期。
    assertThat(1, is(in(new Integer[]{1, 2, 3})));
    //断言可变长数组中存在元素1。
    assertThat(1, oneOf(1, 2, 3));
}

collection下还有关于Iterable接口的几个匹配器,限于篇幅此处就不再赘述了,大家可以在本书代码com.wangwenjun.cicd.chapter02.HamcrestUsageTest中找到其用法细节。

2.2.4 org.hamcrest.number

number下的对象匹配器主要用于匹配Double、BigDecimal和其他实现了Comparable接口类型的对象。

1)IsCloseTo:用于匹配在某个delta范围之内的Double类型的数字。

@Test
public void testIsCloseTo()
{
    /*
    operand - the expected value of matching doubles
    error - the delta (+/-) within which matches will be allowed
    */
    //1.0为期望的操作数,而0.04是delta值。
    assertThat(1.03, is(closeTo(1.0, 0.04)));
}

2)BigDecimalCloseTo:用于匹配在某个delta范围之内的BigDecimal类型的数字。

@Test
public void testBigDecimalCloseTo()
{
    /**
     * operand - the expected value of matching BigDecimals
     * error - the delta (+/-) within which matches will be allowed
     */
    //1.0为期望的操作数,而0.03是delta值。
    assertThat(new BigDecimal("1.03"),
        is(closeTo(new BigDecimal("1.0"), new BigDecimal("0.03")))
    );
}

3)OrderingComparison<T extends Comparable<T>>:用于匹配实现了Comparable接口的类型。

@Test
public void testOrderingComparison()
{
    //2>1
    assertThat(2, greaterThan(1));
    //1>=1
    assertThat(1, greaterThanOrEqualTo(1));
    //1<2
    assertThat(1, lessThan(2));
    //1<=1
    assertThat(1, lessThanOrEqualTo(1));
    //H的ASCII码<W的ASCII码。
    assertThat("Hello", lessThan("World"));
}

2.2.5 org.hamcrest.text

text下的对象匹配器主要用于判断字符串是否相等,以及是否存在包含关系等,它还提供了可以忽略空格、大小写的功能(需要注意的是,其中很多方法已被标记为过期,下面的代码注释中已添加了说明和替代方案,请大家在阅读时多留意)。

@Test
public void testIsEmptyString()
{
    //字符串为空或null,已标记为过期,请使用下一行代码。
    assertThat((String) null, isEmptyOrNullString());
    //与上一行代码等价,但未标记为过期。
    assertThat((String) null, is(emptyOrNullString()));
    //字符串为空,或者已标记为过期,请使用下一行代码。
    assertThat("", isEmptyString());
    //与上一行代码等价,但未标记为过期。
    assertThat("", is(emptyString()));
}
@Test
public void testIsEqualIgnoringCase()
{
    //忽略大小写匹配字符串。
    assertThat("alex", equalToIgnoringCase("ALEX"));
}

@Test
public void testIsEqualIgnoringWhiteSpace()
{
    //忽略空格、制表符匹配字符串,但是该方法已标记为过期方法。
    assertThat("   my\tfoo  bar ", equalToIgnoringWhiteSpace(" my  foo bar"));
    //与上一行代码等价,但未标记为过期方法。
    assertThat("   my\tfoo  bar ",
        equalToCompressingWhiteSpace(" my  foo bar")
    );
}

@Test
public void testStringContainsInOrder()
{
    //断言匹配"alexwangwenjun"中的文本顺序:"alex"在"jun"之前。
    assertThat("alexwangwenjun",
        stringContainsInOrder(Arrays.asList("alex", "jun"))
    );
}

2.2.6 org.hamcrest.xml

xml下的对象匹配器主要使用xpath表达式,对XML文本的节点、内容和命名空间进行相关的匹配操作(笔者将若干个重载的hasXpath方法测试都写在了同一个单元测试方法中,并且附加了详细的注释说明,在实际工作中,建议大家分开编写单元测试代码,同时应尽量避免单元测试的方法太过复杂,以及包含太多的断言语句)。

@Test
public void testHasXPath() throws Exception
{
    //自定义xml命名空间。
    final NamespaceContext ns = new NamespaceContext()
    {
        public String getNamespaceURI(String prefix)
        {
            return "www.wangwenjun.com/profile";
        }
        public String getPrefix(String namespaceURI)
        {
            return "alex";
        }
        public Iterator getPrefixes(String namespaceURI)
        {
            return Arrays.asList("alex").iterator();
        }
    };
    //定义包含命名空间的xml字符串。
    String actuallyXml = "<?xml version = \"1.0\" encoding = \"UTF-8\"?>" +
            "<alex:contact xmlns:alex = \"www.wangwenjun.com/profile\">" +
            "<alex:name>Wangwenjun</alex:name>" +
            "<alex:age>35</alex:age>" +
            "</alex:contact>";

    //将xml解析为Document。
    Document xmlNode = parse(actuallyXml);
    //断言匹配该文档满足"/contact/age" xpath表达式,即包含age节点。
    assertThat(xmlNode, hasXPath("/contact/age"));
    //断言匹配该文档满足"/contact/age" xpath表达式,即包含age节点,同时位于ns命名空间中。
    assertThat(xmlNode, hasXPath("/contact/age", ns));
    //断言匹配该文档满足"/contact/name",且name节点的值为"wangwenjun"(忽略大小写)。
    assertThat(xmlNode, hasXPath("/contact/name", is(equalToIgnoringCase("wangwenjun"))));
    //断言匹配该文档满足"/contact/age",且age节点的值为"35",同时位于ns命名空间中。
    assertThat(xmlNode, hasXPath("/contact/age", ns, equalTo("35")));
}

//将xml字符串解析为Document对象的方法。
private Document parse(String xml) throws Exception
{
    DocumentBuilderFactory documentBuilderFactory =
            DocumentBuilderFactory.newInstance();
    documentBuilderFactory.setNamespaceAware(false);
    DocumentBuilder documentBuilder =
            documentBuilderFactory.newDocumentBuilder();
            return documentBuilder.parse(
            new ByteArrayInputStream(xml.getBytes()));
}