assertj icon indicating copy to clipboard operation
assertj copied to clipboard

Non-informative output of `usingRecursiveComparison().isEqualTo()` for structure containing Set as a field

Open sh2ka opened this issue 1 month ago • 5 comments

Describe the bug

Having recursive comparison of structures that contain sets inside assertJ outputs message that doesn't contain any information about the exact field in actual object that is not equal to the corresponding expected one. It just outputs that two sets are different. Using a debugger I can see it in method that calls addDifference(), but why it doesn't appear in the error message as when the same error appears in list?

  • assertj core version: 3.27.4
  • java version: 17
  • test framework version: junit 5.12.2
  • os (if relevant): windows

Test case reproducing the bug

public class TestAssertJ {

    @Value(staticConstructor = "of")
    private static class SubItem {
        String name;
        String description;
    }

    @Value(staticConstructor = "of")
    private static class Item {
        Integer i;
        String s;
        Set<SubItem> subItems;
    }

    @Data
    private static class Dto {
        private Set<Item> items;
    }

    @Test
    void testRecursiveForSet() {
        final var actual = new Dto();
        actual.items = Set.of(
                Item.of(1, "2", Set.of(SubItem.of("name", "description"))),
                Item.of(3, "4", Set.of(SubItem.of("name", "description")))
        );
        final var expected = new Dto();
        expected.items = Set.of(
                Item.of(1, "2", Set.of(SubItem.of("name", "description"))),
                Item.of(3, "4", Set.of(SubItem.of("name", "another description")))
        );
        assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
    }
}

Output is

field/property 'items' differ:
- actual value  : [TestAssertJ.Item(i=1, s=2, subItems=[TestAssertJ.SubItem(name=name, description=description)]),
    TestAssertJ.Item(i=3, s=4, subItems=[TestAssertJ.SubItem(name=name, description=description)])]
- expected value: [TestAssertJ.Item(i=1, s=2, subItems=[TestAssertJ.SubItem(name=name, description=description)]),
    TestAssertJ.Item(i=3, s=4, subItems=[TestAssertJ.SubItem(name=name, description=another description)])]
The following expected elements were not matched in the actual Set12:
  [TestAssertJ.Item(i=3, s=4, subItems=[TestAssertJ.SubItem(name=name, description=another description)])]

If you change all Set to List you'll get more informative message:

field/property 'items[1].subItems[0].description' differ:
- actual value  : "description"
- expected value: "another description"

sh2ka avatar Nov 20 '25 13:11 sh2ka

Thanks for reporting it, @sh2ka!

Assuming @Data is from Lombok and to make sure we analyze the right example, would you have a chance to delombok your reproducer?

scordio avatar Nov 20 '25 14:11 scordio

With a set that is not ordered, we have to match every actual items with each expected items (o n2 comparison), and thus we can only point out the set diff.

With a list, it is much simpler, we compare the elements in order and thus are able to point out exactly wich one is wrong

joel-costigliola avatar Nov 20 '25 17:11 joel-costigliola

@joel-costigliola , thanks for explanation! Indeed, its not possible that way. Actually, I tested objects with 1 item in a set and didn't take it into consideration. Is there any possibility to map Set into Map by any set of fields (identifiers), i.e. to apply some sort of mapping prior to comparison, so it would be possible to compare them as maps? This would be useful in many cases where items have IDs.

sh2ka avatar Nov 20 '25 18:11 sh2ka

I'm not sure to understand why using maps would be better, moreover what keys would you put in the map ?

joel-costigliola avatar Nov 20 '25 19:11 joel-costigliola

Actually, it can be String, i.e. UUID or any other string that identifies an item. Or even some kind of record. Maps are more useful in this case, because we don't compare with all the expected items in the set, but with those have the same key if any. If the key was not found then we have to rearrange our expected data to make them exist and after that we could have a more informative message when some of the remaining items are not equal like in lists. Of course, this should be some kind of typed mapper that will be applied to any set of that type. For example

assertThat(actual)
    .usingRecursiveComparison()
    .withSetMapping(Person.class, it -> it.stream().collect(Collectors.toMap(Person::getId, Function.identity())))
    .isEqualTo(expected);

Summing up, this could be useful for big DTOs with many fields and recursive structure that have key fields that can be used to simplify error identification.

sh2ka avatar Nov 21 '25 14:11 sh2ka