如何在不定义equals()和hashCode()的情况下比较自定义类的列表?

时间:2017-02-22 09:50:59

标签: java unit-testing assert equality

由于仅为测试目的定义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? */);

4 个答案:

答案 0 :(得分:3)

Hamcrest库有很多用于在集合类型上进行断言的匹配器。特别是,hasItemhasItemscontainscontainsAnyOrder匹配器可以使用匹配器(我喜欢使用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);
}

我希望这会有所帮助,欢呼!