如何断言一个可迭代对象包含具有特定属性的元素?

173

假设我想对具有以下签名的方法进行单元测试:

List<MyItem> getMyItems();
假设 MyItem 是一个 POJO,有许多属性,其中之一是通过 getName() 访问的 "name" 属性。

我只关心验证 List<MyItem> 或任何 Iterable 是否包含两个 MyItem 实例,这两个实例的 "name" 属性分别为 "foo""bar"。如果其他属性不匹配,出于此测试的目的,我并不在意。如果名称匹配,则测试成功。

如果可能的话,我希望它是一行代码。以下是我想要做的类似"伪语法":

assert(listEntriesMatchInAnyOrder(myClass.getMyItems(), property("name"), new String[]{"foo", "bar"});
< p > Hamcrest对于这种类型的任务是否有用?如果是,那么我的伪代码语法的Hamcrest版本会是什么样子呢?< /p >
9个回答

168

感谢 @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"))
));

4
如果您不知道项目的顺序,请改用containsInAnyOrder(来自同一父类) :) - Jorge Campos

113

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

这将会给出:

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

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.assertThat;
import static org.assertj.core.groups.Tuple.tuple;

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

6
不明白为什么这个没有赞。我认为这是迄今为止最好的答案。 - PeMa
2
assertJ库比JUnit断言API更易读。 - Sangimed
@Sangimed 同意,我也更喜欢它胜过 hamcrest。 - davidxxx
在我看来,这样做稍微不太易读,因为它将“实际值”与“期望值”分开,并将它们放在需要匹配的顺序中。 - Terran

66

虽然这不是特别与Hamcrest有关,但我认为在这里值得提一下。在Java8中,我经常使用类似以下的东西:

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

(经 Rodrigo Manyari 稍作修改后编辑。言辞更简洁,评论中可见。)

这段代码可能有一点难以阅读,但我喜欢它的类型和重构安全性。 同时,它也非常适合测试多个Bean属性的组合。例如,在过滤Lambda表达式中使用类似Java的&&表达式。


3
轻微改进: assertTrue(myClass.getMyItems().stream().anyMatch(item -> "foo".equals(item.getName()))); (断言:我的类得到我的项目流中的任何一个匹配项,使其名称等于“foo”) - Rodrigo Manyari
@RodrigoManyari,缺少右括号。 - Abdull
8
这个解决方案浪费了展示适当错误信息的可能性。 - Giulio Caccin
@GiulioCaccin 我认为它不会。如果您使用JUnit,您可以/应该使用重载的断言方法并编写assertTrue(...,"我的自定义测试失败消息"); 更多信息请参见https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Assertions.html#assertTrue(boolean,java.lang.String)。 - Mario Eis
2
我的意思是,如果你对布尔值进行断言,你就失去了自动打印实际/期望差异的能力。虽然可以使用匹配器进行断言,但需要修改此响应以使其类似于此页面中的其他响应。 - Giulio Caccin
嗯,我不太明白你的意思。这里没有差异或比较。问题是集合是否包含项目。所以只需使用assertTrue(...,"集合不包含名称为'foo'的项目"),如果失败,您的测试报告将包含正确的消息。如果由于任何原因您的消息必须包含测试数据,请将其添加到消息中。 - Mario Eis

58

尝试:

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

6
这只是一则旁注,这个解决方案使用的是 Hamcrest 而不是 AssertJ。 - Hartmut Pfarr

31

Assertj 在这方面很好用。

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

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

与 Hamcrest 相比,AssertJ 的一个重要优点在于易于使用代码自动完成功能。


1
另一种方法 Frank: assertThat(list) .containsAll(Arrays.asList(id1,id2)); - Gaurav

9
只要您的List是一个具体类,只要在MyItem上实现了equals()方法,您可以简单地调用contains()方法。
// 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")));

假设您已经实现了一个构造函数,以接受要断言的值。我意识到这不是单行显示,但知道哪个值缺失比同时检查两个值更有用。

1
我真的很喜欢你的解决方案,但他是否应该为测试修改所有那些代码? - Kevin Bowersox
我认为这里的每个答案都需要一些测试设置,执行测试方法,然后断言属性。据我所见,我的答案没有真正的开销,只是我有两个断言在不同的行上,以便于失败的断言可以清楚地识别缺少什么值。 - Brad
最好在assertTrue中包含一条消息,以便错误消息更易于理解。如果没有消息,如果失败,JUnit将只会抛出一个AssertionFailedError而没有任何错误消息。因此最好包含类似于“结果应该包含新的MyItem(\“foo \”)”这样的内容。 - Max
是的,你是对的。无论如何我都会推荐 Hamcrest,而这几天我从不使用 assertTrue()。 - Brad
顺便提一下,您的POJO或DTO应该定义equals方法。 - Tayab Hussain

5
AssertJ 3.9.1 支持在 anyMatch 方法中直接使用断言。
assertThat(collection).anyMatch(element -> element.someProperty.satisfiesSomeCondition())

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

对于简单的条件,我更喜欢使用上面提到的extracting方法,因为得到的可迭代对象可能支持更好可读性的值验证。 例如:它可以提供专门的API,如Frank Neblung的答案中的contains方法。或者您可以随后调用anyMatch并使用方法引用,例如"searchedvalue"::equals。此外,多个提取器可以放入extracting方法中,并使用tuple()进行结果验证。


0

使用 Stream,您还可以执行以下操作:

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

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


0

除了使用hasProperty外,您还可以尝试使用提取函数与hamcrest-more-matcherswhere匹配器。在您的情况下,它将如下所示:

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

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

采用这种方法的优点是:

  • 如果值是在get方法中计算的,则不总是可能通过字段进行验证
  • 如果存在不匹配情况,则应该有带有诊断信息的失败消息(请注意已解决的方法引用MyItem.getName
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"
  • 它适用于Java 8、Java 11和Java 14

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接