由于仅为测试目的定义equals()
和hashCode()
被视为代码气味,我更喜欢使用ReflectionEquals
或自定义匹配器来进行单元测试时比较对象。
但是,我不知道如何在比较用户定义类的列表时使用ReflectionEquals
或自定义匹配器。
例如,如何在未定义equals()
和hashCode()
(可能仅使用ReflectionEquals
或自定义匹配器)的情况下断言以下代码?
// When
List<Record> actual = sut.findBySomeId();
// Then
List<Record> expected = asList(
aRecord()...build(),
aRecord()...build()
);
assertThat(expected, /* how to compare? */);
答案 0 :(得分:3)
Hamcrest库有很多用于在集合类型上进行断言的匹配器。特别是,hasItem
,hasItems
,contains
和containsAnyOrder
匹配器可以使用匹配器(我喜欢使用TypeSafeMatcher)来测试项目收藏品。
我将让您决定哪一个最适合您的需求,但我会使用contains
作为我的例子:
List<Record> actual = sut.findBySomeId();
Record expected1 = aRecord()...build();
Record expected2 = aRecord()...build();
assertThat(actual, contains(matchingRecord(expected1), matchingRecord(expected2));
...
// somewhere the test has access to it
private Matcher<Record> matchingRecord(Record expected) {
return new TypeSafeMatcher<Record>() {
public boolean matchesSafely(Record actual) {
// perform tests and return result, e.g.
return actual.getValue() == expected.getValue();
}
public void describeMismatchSafely(Record record, Description mismatchDescription) {
mismatchDescription.appendText("give a meaningful message");
}
};
}
答案 1 :(得分:2)
有一个名为AssertJ的超级流畅的库可以解决您的问题。它非常易于使用,可以灵活地进行更改,其流畅性使测试易于阅读! 为了使用它,首先您必须导入正确的程序包
import static org.assertj.core.api.Assertions.*;
我不知道您的Record
模型是什么样子,所以我做了一个假人:
public class Record {
private String name;
private String data;
private String history;
public Record(String name, String data, String history) {
this.name = name;
this.data = data;
this.history = history;
}
public String getName() {
return name;
}
public String getData() {
return data;
}
public String getHistory() {
return history;
}
}
然后,如果您只想根据一个字段声明Record
的列表,则测试会想要:
@Test
public void give_when_then() throws Exception {
List<Record> actualList = sut.findBySomeId();
assertThat(actualList)
.extracting(record -> record.getData())
.containsExactly("data1", "data2", "data3");
}
如果要基于多个字段声明对象,则可以执行以下操作:
@Test
public void give_when_then() throws Exception {
List<Record> actualList = sut.findBySomeId();
assertThat(actualList)
.extracting(
record -> record.getName(),
record -> record.getHistory())
.containsExactly(
tuple("name1", "history1"),
tuple("name2", "history2"),
tuple("name3", "history3"));
}
...其中元组也是用于包装结果的assert4J对象。
此外,还有许多断言方法,例如containsExactlyInAnyOrder(...)
,containsAll(...)
,containsAnyOf(...)
等,在某些情况下也可以帮助您的生活。
最后但并非最不重要的一点是,您还可以编写自己的扩展AbstractObjectAssert
基类的特定对象断言类。这样,您将拥有一个名为isEqualToComparingFieldByField
的方法。来自official documentation:
通过属性/字段比较(包括继承的对象),根据属性/字段确定实际对象等于给定对象。 如果相等对象的实现不适合您,这会很方便。
首先,您定义自己的断言类:
public class RecordAssert extends AbstractObjectAssert<RecordAssert, Record> {
public RecordAssert(Record actual) {
super(actual, RecordAssert.class);
}
}
然后它的用法如下:
@Test
public void give_when_then() throws Exception {
List<Record> actual = sut.findBySomeId();
assertThat(actual.get(0)).isEqualToComparingFieldByField(new Record("name1", "data1", "history1"));
// Asserts for other objects
}
希望对您有帮助!
答案 2 :(得分:1)
我想说使用Reflection来检查equals
/ hashCode
本身就是另一种代码味道。
使用Matcher
,您必须执行类似的操作(为清晰起见,我使用了完全限定名称,而是使用import
):这将检查value
result
}与expected
中的那个相同。您可以根据需要添加尽可能多的字段。
assertThat(result, new org.hamcrest.BaseMatcher<MyObject>() {
public boolean matches(MyObject o) {
return java.lang.Objects.equals(o.getValue(), expected.getValue());
}
});
如果你不想为你的类的字段创建一个getter,那么使用默认可见性(Guava和他们使用@VisibleForTesting
注释这些字段的内容)。
您也可以查看AssertJ来创建自定义和流畅的匹配器(它的工作方式或多或少相同)。
答案 3 :(得分:1)
我过去也遇到过这个问题,我完全同意你的看法,实施equals()
和hashCode()
仅用于测试目的是代码气味。我没有使用Hamcrest库,因此我将为您提供一个我在项目中成功使用的自定义解决方案,至于它的用法,我对它非常满意。
public static <E, A> void assertListEquals(BiConsumer<E, A> asserter, List<E> expected, List<A> actual) throws AssertionError {
assertEquals(
"Lists have different sizes. Expected list: " + expected + ", actual list: " + actual,
expected.size(),
actual.size());
for (int i = 0; i < expected.size(); i++) {
try {
asserter.accept(expected.get(i), actual.get(i));
} catch (AssertionError e) {
throw e;
}
}
}
如您所见,使用BiConsumer来应用逻辑来检查两个对象的相等性。所以这个必须在测试类中实现。它的优点是,您可以在比较两个对象时定义您感兴趣的类的字段。
让我们看看它的用法: 首先,我们有一个自定义类,f.e。:Person
public class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
}
然后让我们看一下测试方法:
@Test
public void peopleLiveInTheVillage_findPeople_peopleAreFound() throws Exception {
//Arrange
List<Person> expectedPeople = Arrays.asList(
new Person("Lewis", "Smith", 20),
new Person("Steven", "Richard", 25),
new Person("Richie", "Rich", 30));
//Act
List<Person> actualPeople = sut.findPeople();
//Assert
assertListEquals(
(e, a) -> assertPersonEquals(e, a),
expectedPeople,
actualPeople);
}
private void assertPersonEquals(Person expected, Person actual) {
assertEquals(expected.getFirstName(), actual.getFirstName());
assertEquals(expected.getLastName(), actual.getLastName());
assertEquals(expected.getAge(), actual.getAge());
}
由于我是测试(和TDD)的忠实粉丝,我总是编写大量的测试,所以我总是制作一个帮助方法来包装BiConsumer:
private void assertPeopleEqual(List<Person> expectedPeople, List<Person> actualPeople) throws AssertionError {
assertListEquals(
(e, a) -> assertPersonEqual(e, a),
expectedPeople,
actualPeople);
}
我希望这会有所帮助,欢呼!