如何断言一个 Iterable 包含具有特定属性的元素?

假设我想用这个签名对一个方法进行单元测试:

List<MyItem> getMyItems();

假设 MyItem是一个具有许多属性的 Pojo,其中之一是 "name",通过 getName()访问。

我所关心的只是验证 List<MyItem>或任何 Iterable是否包含两个 MyItem实例,其 "name"属性的值为 "foo""bar"。如果有任何其他属性不匹配,那么我并不关心这个测试的目的。如果名字匹配,就是成功的测试。

如果可能的话,我希望它是一句话。下面是我想要做的一些“伪语法”。

assert(listEntriesMatchInAnyOrder(myClass.getMyItems(), property("name"), new String[]{"foo", "bar"});

Hamcrest 适合做这种事吗?如果是这样,那么上面的伪语法的 Hamcrest 版本到底是什么呢?

211589 次浏览

试试:

assertThat(myClass.getMyItems(),
hasItem(hasProperty("YourProperty", is("YourValue"))));

只要 List 是一个具体的类,只要您在 MyItem 上实现了 equals ()方法,就可以简单地调用   包含()方法。

// given
// some input ... you to complete


// when
List<MyItems> results = service.getMyItems();


// then
assertTrue(results.contains(new MyItem("foo")));
assertTrue(results.contains(new MyItem("bar")));

假设您已经实现了一个构造函数,该构造函数接受要断言的值。我知道这不在一行中,但是知道缺少哪个值比同时检查两个值更有用。

谢谢你@Razvan 给我指明了正确的方向。我能够在一行中得到它,并且成功地找到了 Hamcrest 1.3的导入。

进口:

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;

密码:

assertThat( myClass.getMyItems(), contains(
hasProperty("name", is("foo")),
hasProperty("name", is("bar"))
));

它不是特别的 Hamcrest,但我认为它值得在这里提到。我在 Java8中经常使用的是这样的内容:

assertTrue(myClass.getMyItems().stream().anyMatch(item -> "foo".equals(item.getName())));

(编辑罗德里戈 · 曼亚里(Rodrigo Manyari)的微小改进,不再那么冗长。参见评论。)

它可能有点难以阅读,但我喜欢类型和重构的安全性。 它还可以很酷地组合测试多个 bean 属性,例如在过滤器 lambda 中使用类似 java 的 & & 表达式。

Assertj 擅长这个。

import static org.assertj.core.api.Assertions.assertThat;


assertThat(myClass.getMyItems()).extracting("name").contains("foo", "bar");

与 hamcrest 相比,assertj 有一个很大的优点,那就是代码完成很容易使用。

AssertJ 在 extracting()中提供了一个优秀的特性: 您可以传递 Function来提取字段。它在编译时提供检查。
还可以轻松地首先断言大小。

它将给出:

import static org.assertj.core.api.Assertions;


Assertions.assertThat(myClass.getMyItems())
.hasSize(2)
.extracting(MyItem::getName)
.containsExactlyInAnyOrder("foo", "bar");

containsExactlyInAnyOrder()断言,无论顺序如何,列表都只包含这些值。

要断言列表包含这些值,无论顺序如何,但也可能包含其他值,请使用 contains():

.contains("foo", "bar");

顺便说一句: 要断言来自 List元素的多个字段,使用 AssertJ 时,我们将每个元素的期望值封装到 tuple()函数中:

import static org.assertj.core.api.Assertions;
import static org.assertj.core.groups.Tuple;


Assertions.assertThat(myClass.getMyItems())
.hasSize(2)
.extracting(MyItem::getName, MyItem::getOtherValue)
.containsExactlyInAnyOrder(
tuple("foo", "OtherValueFoo"),
tuple("bar", "OtherValueBar")
);

AssertJ3.9.1支持在 anyMatch方法中直接使用谓词。

assertThat(collection).anyMatch(element -> element.someProperty.satisfiesSomeCondition())

这通常适用于任意复杂条件的用例。

对于简单的条件,我更喜欢使用 extracting方法(见上文) ,因为产生的迭代测试可能支持具有更好可读性的值验证。 示例: 它可以在 Frank Neblung 的回答中提供专门的 API,比如 contains方法。或者您可以稍后在它上面调用 anyMatch,并使用诸如 "searchedvalue"::equals之类的方法引用。在 extracting方法中也可以加入多个萃取器,随后用 tuple()对结果进行验证。

或者,你可以尝试 Hamcrest 更多匹配者 where匹配器与提取功能。在你的情况下,它会像:

import static com.github.seregamorph.hamcrest.MoreMatchers.where;


assertThat(myClass.getMyItems(), contains(
where(MyItem::getName, is("foo")),
where(MyItem::getName, is("bar"))
));

这种方法的优点是:

  • 如果该值是在 get-method 中计算的,则并不总是可以通过字段进行验证
  • 在不匹配的情况下,应该有一个带有诊断信息的故障消息(注意已解决的方法参考 我的项目名称:
Expected: iterable containing [Object that matches is "foo" after call
MyItem.getName, Object that matches is "bar" after call MyItem.getName]
but: item 0: was "wrong-name"
  • 它适用于 Java8、 Java11和 Java14

使用 Stream 你还可以做:

List<String> actual = myList.stream().map(MyClass::getName).collect(toList());
assertThat(actual, hasItem("expectedString1"));

因为对于 anyMatch()allMatch(),您知道列表中的 一些值在列表中,但是有可能您的实际列表只包含5个值,而在 anyMatch()中您有6个; 您不知道是否存在 所有值。使用 hasItem(),您确实可以检查所需的每个值。