6

We have a custom class with several fields, for which we cannot override equals/hashcode methods for business domain reasons

Nevertheless, during unit testing we should assert on whether a collection contains an item of this class

List<CustomClass> customObjectList = classUnderTest.methodUnderTest();
//create customObject with fields set to the very same values as one of the elements in customObjectList
//we should assert here that customObjectList contains customObject

However, so far we did not find any solution that would work without overriding equals/hashcode, e.g. Hamcrest

assertThat(customObjectList, contains(customObject));

results in AssertionError citing

Expected: iterable containing [<CustomClass@578486a3>]
but: item 0: was <CustomClass@551aa95a>

Is there a solution to this without having to compare field-by-field?

hammerfest
  • 2,203
  • 1
  • 20
  • 43

6 Answers6

4

If you are using Java8 you could use Stream#anyMatch and your own customEquals method. Something like this would work -

   assertTrue(customObjectList.stream()
                 .anyMatch(object -> customEquals(object,customObject)));

UPDATED to reflect Holger's comment

John McClean
  • 5,225
  • 1
  • 22
  • 30
  • 1
    `anyMatch` requires a predicate. Fixing that makes the `filter` obsolete: `customObjectList.stream().anyMatch(object -> customEquals(object,customObject))` – Holger Sep 21 '15 at 15:46
  • You're right (thanks!), mentally mixed and matched anyMatch and findFirst. Updated accordingly. FindFirst below, but I think anyMatch is better. assertTrue(customObjectList.stream() .filter(object -> customEquals(object,customObject)) .findFirst().isPresent()); – John McClean Sep 21 '15 at 15:50
4

I would like to say thank you for all the responses, there have been some really good points made

However, what I forgot to mention in my question, is that our custom classes are recursive, that is containing fields of other custom class types, for which the same restriction applies regarding equals and hashcode overriding. Unfortunately neither of the mentioned out-of-the box solutions (AssertJ, Nitor Creations) seem to support deep comparison

Nevertheless, there still seems to be a solution, and that is ReflectionAssert class from Unitils. The following seems to work as we expected, even able to ignore element order in the collection

assertReflectionEquals(Arrays.asList(customObject1, customObject3, customObject2), customObjectList, ReflectionComparatorMode.LENIENT_ORDER);
hammerfest
  • 2,203
  • 1
  • 20
  • 43
3

assertj is good at this. Especially its custom comparison strategy

private static class CustomClass {
    private final String string;

    CustomClass(String string) {
        this.string = string;
    }

    // no equals, no hashCode!
}

@Test
public void assertjToTheRescue() {
    List<CustomClass> list = Arrays.asList(new CustomClass("abc"));

    assertThat(list).usingFieldByFieldElementComparator().contains(new CustomClass("abc"));
}

assertj offers many other usingComparator methods.

Frank Neblung
  • 3,047
  • 17
  • 34
1

Fest Assertions has the following:

assertThat(expected).isEqualsToByComparingFields(actual);

I think it does reflection comparison under the hood. We had a similar issue and this saved us from writing custom comparison logic.

The other thing is to extend an assertion framework of your choice with something for your exact class and case. This approach should shave off some of the performance overhead of using deep reflection comparison.

Danail Alexiev
  • 7,624
  • 3
  • 20
  • 28
  • I can't find this method in the version of fest that I'm using. What version are you using Danail? I have `festAssert: 'org.easytesting:fest-assert-core:2.0M10'`. – fIwJlxSzApHEZIl May 08 '17 at 18:26
  • @anon58192932 I think it should be there. At least I can find in the docs: http://javadox.com/org.easytesting/fest-assert-core/2.0M10/org/fest/assertions/api/ObjectAssert.html#isEqualsToByComparingFields(T) – Danail Alexiev May 08 '17 at 20:19
  • Maybe my imports are off and IntelliJ's autocomplete isn't helping me find them as I'd assume it would. I'll give it another whirl here soon. But can we assume that `assertThat(x).isEqualTo(y)` does NOT use a deep comparison by default? – fIwJlxSzApHEZIl May 09 '17 at 00:33
1

I know two solutions for your problems that are using Hamcrest. The first one tests some properties of the item.

assertThat(customObjectList, contains(allOf(
    hasProperty("propertyA", equalTo("someValue")),
    hasProperty("propertyB", equalTo("anotherValue")))));

Or you can use the reflectEquals matcher from Nitor Creations:

assertThat(customObjectList, contains(reflectEquals(customObject)));
Stefan Birkner
  • 24,059
  • 12
  • 57
  • 72
0

No.

Many methods of the Collection interface are specifically defined in terms of equals(). E.g. Collection#contains():

Returns true if this collection contains the specified element. More formally, returns true if and only if this collection contains at least one element e such that (o==null ? e==null : o.equals(e)).

Just for-each the collection and compare field by field. You can stash the comparing / equaling logic into a static utility class like CustomClasses or similar, and then write a custom Hamcrest matcher.

Alternatively, use a reflective equals, e.g. from Apache Commons Lang, a Hamcrest extension, or (preferably) migrate to AssertJ, it has this functionality out of the box:

assertThat(ImmutableList.of(frodo))
        .usingFieldByFieldElementComparator()
        .contains(frodoClone);

It's hacky and dodgy, but good enough for tests. Please, don't use in production code.

Petr Janeček
  • 37,768
  • 12
  • 121
  • 145