什么是 Hamcrest?
Hamcrest 是一个匹配器库,它提供了一系列的匹配器(matchers),这些匹配器可以组合在一起,形成在测试中表达意图的灵活表达式。
它们也被用于其他目的。
介绍
Hamcrest 是一个用于编写匹配器对象的框架,允许以声明方式定义“匹配”规则。
有许多情况下匹配器非常有价值,例如 UI 验证或数据过滤,但在编写灵活测试的领域中,匹配器最常被使用。本教程将向您展示如何在单元测试中使用 Hamcrest。
在编写测试时,有时很难在过度指定测试(使其对变化变得脆弱)和不够指定测试(使测试在被测试的事物出现问题时仍然能够通过)之间找到平衡。
拥有一个工具,可以精确选择测试中的被测方面并描述它应该具有的值,以受控的精度级别,有助于编写“刚刚好”的测试。
这样的测试在被测试方面的行为偏离期望行为时会失败,但在对行为进行微小且无关的更改时仍然会通过。
我的第一个 Hamcrest 测试
我们将首先编写一个非常简单的 JUnit 5 测试,但是与其使用 JUnit 的 assertEquals 方法不同,我们使用 Hamcrest 的 assertThat 结构和标准的匹配器集合,它们都是通过静态导入导入的:
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class BiscuitTest {
@Test
public void testEquals() {
Biscuit theBiscuit = new Biscuit("Ginger");
Biscuit myBiscuit = new Biscuit("Ginger");
assertThat(theBiscuit, equalTo(myBiscuit));
}
}
assertThat
方法是一个用于进行测试断言的风格化语句。在这个例子中,断言的主体是作为第一个方法参数的对象 biscuit
。第二个方法参数是用于 Biscuit 对象的匹配器,这里使用的是一个匹配器,通过使用对象的 equals
方法检查一个对象是否等于另一个对象。由于 Biscuit 类定义了一个 equals
方法,所以测试通过。
如果在测试中有多个断言,您可以在断言中包含被测试值的标识符:
assertThat("chocolate chips", theBiscuit.getChocolateChipCount(), equalTo(10));
assertThat("hazelnuts", theBiscuit.getHazelnutCount(), equalTo(3));
其他测试框架
Hamcrest 从一开始就被设计成与不同的单元测试框架集成。
例如,Hamcrest 可以与 JUnit(所有版本)和 TestNG 一起使用(有关详细信息,请查看随完整的 Hamcrest 发行版提供的示例)。
在现有的测试套件中迁移到使用 Hamcrest 风格的断言是相当容易的,因为其他断言风格可以与 Hamcrest 并存。
Hamcrest 还可以与模拟对象框架一起使用,通过使用适配器将模拟对象框架的匹配器概念桥接到 Hamcrest 匹配器。
例如,JMock 1 的约束就是 Hamcrest 的匹配器。
Hamcrest 提供了一个 JMock 1 适配器,允许您在 JMock 1 测试中使用 Hamcrest 匹配器。
JMock 2 不需要这样的适配器层,因为它被设计为使用 Hamcrest 作为其匹配库。
Hamcrest 还为 EasyMock 2 提供了适配器。同样,详细信息请参阅 Hamcrest 的示例。
常见匹配器一览
Hamcrest 提供了一个有用的匹配器库。
以下是其中一些最重要的匹配器。
核心(Core)
anything
- 总是匹配,如果您不关心被测试对象是什么,这很有用。describedAs
- 用于添加自定义失败描述的修饰器。is
- 用于提高可读性的修饰器 - 请参阅下面的“语法糖”。
逻辑(Logical)
allOf
- 如果所有匹配器都匹配则匹配,短路(类似于 Java 的 &&)。-
anyOf
- 如果任何匹配器匹配则匹配,短路(类似于 Java 的)。 not
- 如果包装的匹配器不匹配,则匹配,反之亦然。
对象(Object)
equalTo
- 使用Object.equals
测试对象相等性。hasToString
- 测试Object.toString
。instanceOf
,isCompatibleType
- 测试类型。notNullValue
,nullValue
- 测试是否为 null。sameInstance
- 测试对象身份。
JavaBeans
hasProperty
- 测试 JavaBeans 属性。
集合(Collections)
array
- 使用一组匹配器测试数组的元素。hasEntry
,hasKey
,hasValue
- 测试映射是否包含条目、键或值。hasItem
,hasItems
- 测试集合是否包含元素。hasItemInArray
- 测试数组是否包含元素。
数字(Number)
closeTo
- 测试浮点数值是否接近给定值。greaterThan
,greaterThanOrEqualTo
,lessThan
,lessThanOrEqualTo
- 测试顺序。
文本(Text)
equalToIgnoringCase
- 忽略大小写测试字符串相等性。equalToIgnoringWhiteSpace
- 忽略空白字符差异测试字符串相等性。containsString
,endsWith
,startsWith
- 测试字符串匹配。
语法糖(Sugar)
Hamcrest 力求使您的测试尽可能易读。例如,is
匹配器是一个不对基础匹配器添加任何额外行为的包装器。以下断言都是等价的:
assertThat(theBiscuit, equalTo(myBiscuit));
assertThat(theBiscuit, is(equalTo(myBiscuit)));
assertThat(theBiscuit, is(myBiscuit));
最后一种形式是允许的,因为 is(T value)
被重载为返回 is(equalTo(value))
。
编写自定义匹配器
Hamcrest 包含许多有用的匹配器,但您可能会发现,为了满足您的测试需求,有时需要不时地创建自己的匹配器。
这通常发生在您找到一段代码片段,该代码片段一遍又一遍地测试相同的一组属性(在不同的测试中),并且您想将该片段捆绑到单个断言中。
通过编写自己的匹配器,您将消除代码重复,使您的测试更易读!
让我们编写一个自己的匹配器,用于测试 double 值是否为 NaN(不是一个数字)。这是我们想要编写的测试:
@Test
public void testSquareRootOfMinusOneIsNotANumber() {
assertThat(Math.sqrt(-1), is(notANumber()));
}
以下是实现:
package org.hamcrest.examples.tutorial;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
public class IsNotANumber extends TypeSafeMatcher<Double> {
@Override
public boolean matchesSafely(Double number) {
return number.isNaN();
}
public void describeTo(Description description) {
description.appendText("not a number");
}
public static Matcher<Double> notANumber() {
return new IsNotANumber();
}
}
assertThat
方法是一个通用方法,它接受一个由断言主体类型参数化的 Matcher。
我们正在断言关于 Double 值的事情,所以我们知道我们需要一个 Matcher<Double>
。对于我们的 Matcher 实现,最方便的方法是继承 TypeSafeMatcher
,它为我们执行到 Double 的强制转换。
我们只需要实现 matchesSafely
方法 - 它简单地检查 Double 是否为 NaN - 以及 describeTo
方法 - 用于在测试失败时生成失败消息。
以下是测试失败消息的示例:
assertThat(1.0, is(notANumber()));
// 失败,带有消息
java.lang.AssertionError: Expected: is not a number got : <1.0>
我们的匹配器中的第三个方法是一个方便的工厂方法。我们通过静态导入此方法在测试中使用匹配器:
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.examples.tutorial.IsNotANumber.notANumber;
public class NumberTest {
@Test
public void testSquareRootOfMinusOneIsNotANumber() {
assertThat(Math.sqrt(-1), is(notANumber()));
}
}
尽管 notANumber
方法每次调用都会创建一个新的匹配器,但您不应假设这是您的匹配器的唯一使用模式。
因此,您应确保您的匹配器是无状态的,以便可以在匹配之间重用单个实例。
参考资料
https://github.com/hamcrest/JavaHamcrest
https://hamcrest.org/JavaHamcrest/tutorial