单元测试利器——Hamcrest

1. 概述

最近在翻阅 Spring 源码的时候,发现它引入了一个新的框架来进行单元测试——Hamcrest 。查阅相关的资料学习了一下,发现确实是一个非常简洁高效的测试框架,它捆绑在JUnit中。

记录一下探索Hamcrest API并学习的过程,让更多的人了解到这种直观的单元测试。

2. Hamcrest依赖

我们可以通过在pom.xml文件中添加以下依赖项来将Hamcrest与maven一起使用:

1
2
3
4
5
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
</dependency>

可以在此处找到此库的最新版本。

3. Demo

Hamcrest通常与junit和其他测试框架一起用于进行断言。具体来说,我们只使用API的单个assertThat语句与适当的匹配器,而不是使用junit的众多assert方法。

让我们看一个测试两个 String 的相似性的例子,以便清楚地了解Hamcrest如何进行测试:

1
2
3
4
5
6
7
8
9
public class StringMatcherTest {

@Test
public void given2Strings_whenEqual_thenCorrect() {
String a = "foo";
String b = "FOO";
assertThat(a, equalToIgnoringCase(b));
}
}

在接下来的部分中,我们将介绍Hamcrest提供的其他几个常见匹配器。

4. Class 匹配器

Hamcrest提供了用于在任意Java对象上进行断言的匹配器。

断言 ObjecttoString 方法返回指定的 String

1
2
3
4
5
6
@Test
public void givenBean_whenToStringReturnsRequiredString_thenCorrect(){
Person person=new Person("Barrack", "Washington");
String str=person.toString();
assertThat(person,hasToString(str));
}

我们还可以检查一个类是另一个类的子类:

1
2
3
4
5
@Test
public void given2Classes_whenOneInheritsFromOther_thenCorrect(){
assertThat(Cat.class,typeCompatibleWith(Animal.class));
}
}

5. bean 匹配器

我们可以使用Hamcrest的Bean匹配器来检查Java bean的属性。

假设以下 Person bean:

1
2
3
4
5
6
7
8
9
public class Person {
String name;
String address;

public Person(String personName, String personAddress) {
name = personName;
address = personAddress;
}
}

我们可以检查bean是否具有name 属性:

1
2
3
4
5
@Test
public void givenBean_whenHasValue_thenCorrect() {
Person person = new Person("Baeldung", 25);
assertThat(person, hasProperty("name"));
}

我们还可以检查 Person 是否具有地址属性,并且值为纽约:

1
2
3
4
5
@Test
public void givenBean_whenHasCorrectValue_thenCorrect() {
Person person = new Person("Baeldung", "New York");
assertThat(person, hasProperty("address", equalTo("New York")));
}

我们还可以检查是否使用相同的值构造了两个 Person 对象:

1
2
3
4
5
6
@Test
public void given2Beans_whenHavingSameValues_thenCorrect() {
Person person1 = new Person("Baeldung", "New York");
Person person2 = new Person("Baeldung", "New York");
assertThat(person1, samePropertyValuesAs(person2));
}

6. Collection 匹配器

Hamcrest提供用于检查Collection 的匹配器。

简单检查一下Collection是否为空:

1
2
3
4
5
@Test
public void givenCollection_whenEmpty_thenCorrect() {
List<String> emptyList = new ArrayList<>();
assertThat(emptyList, empty());
}

要检查Collection 的大小:

1
2
3
4
5
6
@Test
public void givenAList_whenChecksSize_thenCorrect() {
List<String> hamcrestMatchers = Arrays.asList(
"collections", "beans", "text", "number");
assertThat(hamcrestMatchers, hasSize(4));
}

当然,对于数组也有类似的方法:

1
2
3
4
5
@Test
public void givenArray_whenChecksSize_thenCorrect() {
String[] hamcrestMatchers = { "collections", "beans", "text", "number" };
assertThat(hamcrestMatchers, arrayWithSize(4));
}

检查Collection是否包含给定成员(无论顺序如何):

1
2
3
4
5
6
7
@Test
public void givenAListAndValues_whenChecksListForGivenValues_thenCorrect() {
List<String> hamcrestMatchers = Arrays.asList(
"collections", "beans", "text", "number");
assertThat(hamcrestMatchers,
containsInAnyOrder("beans", "text", "collections", "number"));
}

进一步断言Collection 成员(按给定顺序排列):

1
2
3
4
5
6
7
@Test
public void givenAListAndValues_whenChecksListForGivenValuesWithOrder_thenCorrect() {
List<String> hamcrestMatchers = Arrays.asList(
"collections", "beans", "text", "number");
assertThat(hamcrestMatchers,
contains("collections", "beans", "text", "number"));
}

要检查数组是否包含给定元素:

1
2
3
4
5
@Test
public void givenArrayAndValue_whenValueFoundInArray_thenCorrect() {
String[] hamcrestMatchers = { "collections", "beans", "text", "number" };
assertThat(hamcrestMatchers, hasItemInArray("text"));
}

我们也可以反过来,测试某个元素是否存在于某个 Collection 中:

1
2
3
4
5
@Test
public void givenValueAndArray_whenValueIsOneOfArrayElements_thenCorrect() {
String[] hamcrestMatchers = { "collections", "beans", "text", "number" };
assertThat("text", isOneOf(hamcrestMatchers));
}

另外一个相似的 API 如下:

1
2
3
4
5
6
@Test
public void givenValueAndArray_whenValueFoundInArray_thenCorrect() {
String[] array = new String[] { "collections", "beans", "text",
"number" };
assertThat("beans", isIn(array));
}

类似的,对于数组也有这样的 API,检查数组是否包含某个元素(顺序无关):

1
2
3
4
5
6
7
@Test
public void givenArrayAndValues_whenValuesFoundInArray_thenCorrect() {
String[] hamcrestMatchers = { "collections", "beans", "text", "number" };
assertThat(hamcrestMatchers,
arrayContainingInAnyOrder("beans", "collections", "number",
"text"));
}

要检查数组是否包含给定元素但是按给定顺序:

1
2
3
4
5
6
@Test
public void givenArrayAndValues_whenValuesFoundInArrayInOrder_thenCorrect() {
String[] hamcrestMatchers = { "collections", "beans", "text", "number" };
assertThat(hamcrestMatchers,
arrayContaining("collections", "beans", "text", "number"));
}

如果是 Map ,可以使用以下匹配器:

要检查它是否包含给定的 key:

1
2
3
4
5
6
@Test
public void givenMapAndKey_whenKeyFoundInMap_thenCorrect() {
Map<String, String> map = new HashMap<>();
map.put("blogname", "baeldung");
assertThat(map, hasKey("blogname"));
}

和给定的 value:

1
2
3
4
5
6
@Test
public void givenMapAndValue_whenValueFoundInMap_thenCorrect() {
Map<String, String> map = new HashMap<>();
map.put("blogname", "baeldung");
assertThat(map, hasValue("baeldung"));
}

检查 Entry

1
2
3
4
5
6
@Test
public void givenMapAndEntry_whenEntryFoundInMap_thenCorrect() {
Map<String, String> map = new HashMap<>();
map.put("blogname", "baeldung");
assertThat(map, hasEntry("blogname", "baeldung"));
}

7. Number 匹配器

Numerb 匹配器是用来对变量进行数字相关的断言。

要检查 greatThen 条件:

1
2
3
4
@Test
public void givenAnInteger_whenGreaterThan0_thenCorrect() {
assertThat(1, greaterThan(0));
}

检查 greaterThanequalTo 条件:

1
2
3
4
@Test
public void givenAnInteger_whenGreaterThanOrEqTo5_thenCorrect() {
assertThat(5, greaterThanOrEqualTo(5));
}

检查 lessThan 条件:

1
2
3
4
@Test
public void givenAnInteger_whenLessThan0_thenCorrect() {
assertThat(-1, lessThan(0));
}

检查 lessThanequalTo 条件:

1
2
3
4
@Test
public void givenAnInteger_whenLessThanOrEqTo5_thenCorrect() {
assertThat(-1, lessThanOrEqualTo(5));
}

要检查 closeTo 条件:

1
2
3
4
@Test
public void givenADouble_whenCloseTo_thenCorrect() {
assertThat(1.2, closeTo(1, 0.5));
}

这个检查方法特别的有意思,但是个人认为适用的场景并不是很多,它是用来测试 某个数字是否约等于基准值(第二个参数为误差范围)

8. Text 匹配器

使用Hamcrest的文本匹配器,对String的断言变得更容易,更整洁,更直观。

要检查 String 是否为空:

1
2
3
4
5
@Test
public void givenString_whenEmpty_thenCorrect() {
String str = "";
assertThat(str, isEmptyString());
}

要检查 String 是空还是 null

1
2
3
4
5
@Test
public void givenString_whenEmptyOrNull_thenCorrect() {
String str = null;
assertThat(str, isEmptyOrNullString());
}

检查两个String的是否相等(忽略空格):

1
2
3
4
5
6
@Test
public void given2Strings_whenEqualRegardlessWhiteSpace_thenCorrect() {
String str1 = "text";
String str2 = " text ";
assertThat(str1, equalToIgnoringWhiteSpace(str2));
}

我们还可以检查给定顺序中给定 String 中是否存在一个或多个子字符串:

1
2
3
4
5
@Test
public void givenString_whenContainsGivenSubstring_thenCorrect() {
String str = "calligraphy";
assertThat(str, stringContainsInOrder(Arrays.asList("call", "graph")));
}

检查两个 String 是否相等(忽略大小写):

1
2
3
4
5
6
@Test
public void given2Strings_whenEqual_thenCorrect() {
String a = "foo";
String b = "FOO";
assertThat(a, equalToIgnoringCase(b));
}

9. 核心API

虽然 Hamcrest 核心API是通过第三方框架提供的。但是,它为我们提供了一些很好的结构,使我们的单元测试更具可读性,还有一些可以轻松使用的核心匹配器。

9.1 is

1
2
3
4
5
6
@Test
public void given2Strings_whenIsEqualRegardlessWhiteSpace_thenCorrect() {
String str1 = "text";
String str2 = " text ";
assertThat(str1, is(equalToIgnoringWhiteSpace(str2)));
}

9.2 not

1
2
3
4
5
6
@Test
public void given2Strings_whenIsNotEqualRegardlessWhiteSpace_thenCorrect() {
String str1 = "text";
String str2 = " texts ";
assertThat(str1, not(equalToIgnoringWhiteSpace(str2)));
}

9.3 String

检查String是否包含给定的子字符串:

1
2
3
4
5
6
@Test
public void givenAStrings_whenContainsAnotherGivenString_thenCorrect() {
String str1 = "calligraphy";
String str2 = "call";
assertThat(str1, containsString(str2));
}

检查String是否以给定的子字符串开头:

1
2
3
4
5
public void givenAString_whenStartsWithAnotherGivenString_thenCorrect() {
String str1 = "calligraphy";
String str2 = "call";
assertThat(str1, startsWith(str2));
}

检查String是否以给定的子字符串结尾:

1
2
3
4
5
6
@Test
public void givenAString_whenEndsWithAnotherGivenString_thenCorrect() {
String str1 = "calligraphy";
String str2 = "phy";
assertThat(str1, endsWith(str2));
}

9.4 Object

检查两个Object是否属于同一个实例:

1
2
3
4
5
@Test
public void given2Objects_whenSameInstance_thenCorrect() {
Cat cat=new Cat();
assertThat(cat, sameInstance(cat));
}

检查Object是否是给定类的实例:

1
2
3
4
5
@Test
public void givenAnObject_whenInstanceOfGivenClass_thenCorrect() {
Cat cat=new Cat();
assertThat(cat, instanceOf(Cat.class));
}

9.5 Collection

1
2
3
4
5
6
@Test
public void givenList_whenEachElementGreaterThan0_thenCorrect() {
List<Integer> list = Arrays.asList(1, 2, 3);
int baseCase = 0;
assertThat(list, everyItem(greaterThan(baseCase)));
}

9.6 断言链

anyOf:当目标满足任何条件时测试通过,类似于逻辑OR:

1
2
3
4
5
6
7
@Test
public void givenString_whenMeetsAnyOfGivenConditions_thenCorrect() {
String str = "calligraphy";
String start = "call";
String end = "foo";
assertThat(str, anyOf(startsWith(start), containsString(end)));
}

allOf:只有在目标满足所有条件时才通过测试,类似于逻辑AND:

1
2
3
4
5
6
7
@Test
public void givenString_whenMeetsAllOfGivenConditions_thenCorrect() {
String str = "calligraphy";
String start = "call";
String end = "phy";
assertThat(str, allOf(startsWith(start), endsWith(end)));
}

10.自定义匹配器

我们可以通过扩展TypeSafeMatcher来定义我们自己的匹配器。比如,我们将创建一个自定义匹配器,只有当目标是正整数时才允许测试通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IsPositiveInteger extends TypeSafeMatcher<Integer> {

public void describeTo(Description description) {
description.appendText("a positive integer");
}

@Factory
public static Matcher<Integer> isAPositiveInteger() {
return new IsPositiveInteger();
}

@Override
protected boolean matchesSafely(Integer integer) {
return integer > 0;
}

}

我们只需要实现 matchSafely 方法来检查目标是否确实是一个正整数,而 describeTo 方法在测试未通过的情况下产生失败消息。

测试:

1
2
3
4
5
@Test
public void givenInteger_whenAPositiveValue_thenCorrect() {
int num = 1;
assertThat(num, isAPositiveInteger());
}

如果我们验证一个负数,错误信息将如下所示:

1
java.lang.AssertionError: Expected: a positive integer but: was <-1>