
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中都会讲解核心匹配器的使用。

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