可以向 AssertJ assertThat 添加自定义消息吗?

我们有一个主要使用 JUnit 断言和 Hamcrest 匹配器的测试套件。我们的一个团队开始尝试使用 断言,它的语法、灵活性和声明性给人们留下了深刻的印象。JUnit 提供了一个我在 AssertJ 中找不到等价的特性: 添加自定义断言失败消息。

我们经常比较那些不适合人类阅读的对象,它们看起来像是随机 ID 或 UUID,根据它们包含的数据不可能知道它们应该是什么。遗憾的是,对于我们的代码库来说,这是一种不可避免的情况,因为它实现的部分目的是在其他服务之间映射数据,而不一定了解它是什么。

在 JUnit 中,assertThat方法在 Matcher<T>参数之前提供一个带有 String reason参数的版本。这使得添加一个简短的调试字符串来解决这个问题变得非常容易,比如比较对于人类应该意味着什么。

另一方面,AssertJ 提供了大量不同的 广义 static assertThat方法,它们返回某种形式的 接口断言或其众多实现类之一。此接口不提供将自定义消息设置为包含在故障中的标准方法。

有没有什么方法可以从 AssertJAPI 或其某个扩展获得这种功能,而不需要向 为每个断言类型创建一个自定义断言类添加消息?

52805 次浏览

以经典的方式,我在发布问题后找到了我要找的东西。希望这能让下一个人更容易找到而不用事先知道它叫什么。这个神奇的方法是名称短小的 as,它是 AbstractAssert实现的另一个接口的一部分: 可以描述,而不是基础 Assert 接口。

public S as(String description, Object... args)

设置支持 String.format(String, Object...)语法的此对象的说明。
例如:

try {
// set a bad age to Mr Frodo which is really 33 years old.
frodo.setAge(50);
// you can specify a test description with as() method or describedAs(), it supports String format args
assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);
} catch (AssertionError e) {
assertThat(e).hasMessage("[check Frodo's age] expected:<[33]> but was:<[50]>");
}

如果断言失败,则 catch 块 hasMessage中的引号字符串将显示在单元测试输出日志中。


我发现这一点通过注意到 自定义断言页中的 failWithMessage助手链接在问题中。该方法的 JavaDoc指出它是受保护的,因此调用方不能使用它来设置自定义消息。不过,它确实提到了 as助手:

此外,此方法支持使用 as(String, Object...)设置的任何描述或使用 overridingErrorMessage(String, Object...)的用户定义的重写错误消息。

... 和 覆盖错误信息助手,它完全用提供的新字符串替换标准 AssertJexpected: ... but was:...消息。

AssertJ 主页直到特性突出显示页面才提到这两个助手,该页面在 软性断言部分中显示了 as助手的示例,但是没有直接描述它的功能。

在帕特里克 · M 的回答中添加另一个选项:

除了使用 Descriptable.as,你也可以使用 AbstractAssert.withFailMessage():

try {
// set a bad age to Mr Frodo which is really 33 years old.
frodo.setAge(50);
// you can specify a test description via withFailMessage(), supports String format args
assertThat(frodo.getAge()).
withFailMessage("Frodo's age is wrong: %s years, difference %s years",
frodo.getAge(), frodo.getAge()-33).
isEqualTo(33);
} catch (AssertionError e) {
assertThat(e).hasMessage("Frodo's age is wrong: 50 years, difference 17 years");
}

使用 Descriptable.as的不同之处在于它提供了 完全控制自定义消息-没有“预期”和“但是”。

当测试的实际值对于表示没有用处时,这种方法非常有用——这种方法允许您显示其他可能计算出来的值,或者根本不显示。


重要提示 ,就像 Descriptable.as一样,您必须调用 withFailMessage() 之前任何实际断言-否则它将无法工作,因为断言将首先触发。Javadoc 中提到了这一点。

使用 AssertJ 中的内置 as()方法,例如:

 assertThat(myTest).as("The test microservice is not active").isEqualTo("active");

到目前为止提到的两个选项是 aswithFailMessage,因此我将不再讨论语法或用法。要了解它们之间的区别,以及它们如何变得有用,请考虑我们正在导出的测试度量的用例:

// map of all metrics, keyed by metrics name
Map<String, Double> invocations = ...


List.of(
"grpc.client.requests.sent",
"grpc.client.responses.received",
"grpc.server.requests.received",
"grpc.server.responses.sent"
).forEach { counter ->
var meter = // create meter name using counter
assertThat(invocations)
.withFailMessage("Meter %s is not found", meter)
.containsKey(meter)
assertThat(invocations.get(meter))
.as(meter)
.isEqualTo(0.0)
}

我使用 Java11语法来减少一些样板文件。

如果没有 withFailMessage,如果在地图中不存在米,那么默认输出包含地图中所有条目的转储,这会使测试日志变得混乱。我们不在乎还有什么其他的仪表,只要我们想要的就在那里。

使用 withFailMessage,输出变成:

java.lang.AssertionError: Meter blah is not found

对于 as,它只将给定的消息附加到输出,但保留失败的比较,这非常有用。我们得到:

org.opentest4j.AssertionFailedError: [blah]
Expecting:
<1.0>
to be equal to:
<0.0>
but was not.