Hamcrest是用于编写匹配器对象的框架,允许以声明方式定义“匹配”规则。有许多情况下匹配器是不可估量的,例如UI验证或数据过滤,但是在编写灵活测试的领域中,匹配器反而是最常用的。

简述

Hamcrest是用于编写匹配器对象的框架,允许以声明方式定义“匹配”规则。有许多情况下匹配器是不可估量的,例如UI验证或数据过滤,但是在编写灵活测试的领域中,匹配器反而是最常用的。

Hamcrest还是相对比较简单的,API也相对比较少,就从一个小例子说起吧。

假如要你要测试一个集合中是否包含三个元素中的一个,如果包含则断言真,否则为假。把集合的初始化放在@Before中,则用JUnit的Assert断言写法如下:

@RunWith(JUnit4.class)
public class HamcrestTest {
private List<String> hamcrestTestList;

@Before
public void setUp() {
hamcrestTestList = new ArrayList<>();
hamcrestTestList.add("first element");
hamcrestTestList.add("second element");
hamcrestTestList.add("third element");
}

@Test
public void assertWithJunitTest() {
assertTrue(hamcrestTestList.contains("first element")
|| hamcrestTestList.contains("second element")
|| hamcrestTestList.contains("third element"));
}
}

assertWithJunitTest方法本身并难以理解,但是你第一眼看到它很可能不太明白它是做什么的,而且代码也不简练,而Hamcrest则正是为了简化断言,可以构建测试表达式的匹配器库。Hamcrest的书写方法如下:

import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;

...

@Test
public void assertWithHamcrestTest(){
assertThat(hamcrestTestList, hasItem(anyOf(equalTo("first element"), equalTo("second element"), equalTo("third element"))));
}

常用API

hamcrest的匹配器都看起来很好理解,并且还提供了自定义匹配器的接口,可以满足编写代码的需要。主要的API接口有如下:

核心

  • anything - 绝对匹配,无论什么情况下都会匹配成功;
  • describedAs - 添加自定义失败描述;
  • is - 是否的意思,仅用来改善断言语句的可读性;

逻辑

  • allOf - 检查是否包含所有的匹配器,相当于与(&&);
  • anyOf - 检查是否包含匹配器中的一个,相当于(||);
  • not - 检查是否与匹配器相反,相当于非(!);

对象

  • equalTo - 检查两个对象是否相等;
  • hasToString - 检查Object.toString;
  • instanceOf,isCompatibleType - 检查对象是否是兼容类型;
  • notNullValue,nullValue - 检查是否是null值;
  • sameInstance - 检查对象是否是相同的类型;

Beans

  • hasProperty - 检查对象是否有某种属性;

集合

  • array - 检查array的元素是否和匹配器描述的相同;
  • hasEntry,hasKey,hasValue - 测试给定的Map是否有特定的实体、键或者值;
  • hasItem,hasItems - 测试集合是否有一个或者多个元素;
  • hasItemInArray - 测试数组中是否有某一元素;

数字

  • closeTo - 给定的数字是否接近于给定的值;
  • greaterThan,greaterThanOrEqualTo,lessThan,lessThanOrEqualTo -给定的数字是否大于、大于等于、小于、小于等于给定的值;

文本

  • equalToIgnoringCase - 检查给定的字符串是否与另一字符串在忽略大小写的情况下相同;
  • equalToIgnoringWhiteSpace - 检查给定的字符串是否与另一字符串在忽略空格的情况下相同;
  • containsString - 检查给定的字符串是否包含某一字符串;
  • endsWith - 检查给定的字符串是否以某一字符串结尾;
  • startsWith - 检查给定的字符串是否以某一字符串开头;

这些匹配器除了可以单独使用外,还可以组合使用,提供更佳精确的匹配。例如:

assertThat(hamcrestTestList, hasItem(anyOf(equalTo("first element"),equalTo("second element"), equalTo("third element"))));

自定义匹配器

hamcrest除了可以使用上述匹配器以外,还可以自己编写合适的匹配器,更好的进行测试。自定义匹配器需要实现Mather接口和一个适当的工厂方法。自定义匹配器一方面可以达到合适的匹配,另一方面也可以简化断言语句。

要想定义自定义匹配器,则需要实现BaseMatcher<T>或者TypeSafeMatcher<T>,需要在build.gradle文件中dependencies闭包中引入对应的包:

dependencies {
testImplementation 'org.hamcrest:hamcrest-core:1.3'
}

自定义匹配器有以下两种实现方法:

实现BaseMatcher<T>接口

如果实现了BaseMatcher<T>,那么需要重写matches方法,matches方法内部实现我们的逻辑,即要满足什么条件时匹配器将匹配各种条件,并确定是否匹配成功。比如如果要实现自定义匹配器,判断给定的字符串是否以ham开头,并且以java结尾,则自定义匹配器实现方式如下:

public class IsExceptedStringMatcher extends BaseMatcher<String> {
@Factory
public static <T> Matcher<String> isExceptedString() {
return new IsExceptedStringMatcher();
}

@Override
public boolean matches(Object item) {
String str = (String) item;
if (str != null && str.length() >= 7 && str.startsWith("ham") && str.endsWith("java")) {
return true;
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("a string that start with \"ham\" and end with \"java\"");
}
}

之后就可以在断言中直接使用自定义匹配器:

@Test(expected = AssertionError.class)
public void isExceptedStringTest(){
String str="hamcrestjava";
assertThat(str,isExceptedString());
str=null;
assertThat(str,isExceptedString());
}

实现TypeSafeMatcher<T>接口

如果实现了TypeSafeMatcher <T>,那么需要重写matchesSafely方法,

protected boolean matchesSafely(T item)

matchesSafely方法内部同样实现我们的逻辑,该方法绝不能接受一个null对象,它总会接受一个类型为T的参数,该参数已被检查是否为null且永远不能为null。如果我们要在自定义匹配器内部进行是否为null检查,则要继续实现BaseMatcher<T>接口,如果要自定义一个密码检查的匹配器,要求密码必须包含1个特殊符号(!、"、#、$、%、&、’、(、)、*、+、-、.、/)和1个数字,并且密码长度至少为6,则自定义匹配器实现方式如下:

public class IsStrongPassword extends TypeSafeMatcher<String> {
@Factory
public static <T> Matcher<String> isStrongPassword() {
return new IsStrongPassword();
}

@Override
protected boolean matchesSafely(String item) {
if (containsSymbol(item) && containsDigit(item) && item.length() >= 6) {
return true;
}
return false;
}

private boolean containsDigit(String password) {
for (char ch : password.toCharArray()) {
if (Character.isDigit(ch)) {
return true;
}
}
return false;
}

private boolean containsSymbol(String password) {
for (char ch : password.toCharArray()) {
if ((int) ch >= 33 && (int) ch <= 47) {
return true;
}
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("a string which a strong password");
}
}

接下来你同样可以在测试代码中使用如下的方式使用该匹配器:

@Test(expected = AssertionError.class)
public void IsStrongPasswordTest() {
String password;
password = "!1s2dxs";
assertThat(password, isStrongPassword());
password = "123dsda";
assertThat(password, isStrongPassword());
}