Skip to content

Recursive comparison api improvements #1002

Closed
@joel-costigliola

Description

@joel-costigliola

Recursive comparison api

Here's an example to give users an idea of what it looks like:

assertThat(person).usingRecursiveComparison()
                  .ignoringNullFields()
                  .ignoringFields("name", "age")                                                 
                  .forcingRecursiveComparisonFor(Child.class, Sister.class)
                  .isEqualTo(expectedPerson);

This will be in Beta as it might change according to people's feedback.

Requirements

Recursive comparison Fluent API configuration

Implements an easy to use/discover API as in:

assertThat(person).usingRecursiveComparison()
                  .ignoringNullFields()
                  .ignoringFields("name", "age")                                                 
                  .isEqualTo(expectedPerson);
  • expose ignoring actual null fields
  • expose ignoring fields
  • expose ignoring overridden equals
  • expose setting a comparison strategy per fields/types
  • expose enabling strict type checking
  • documentation

Ignoring fields in comparison

Users can specify the comparison to ignore all null fields in the object under test.

Users can specify to ignore specific fields in the object under test:

  • giving their full path, ex: person.sister.name
  • by regex, ex: .*name means name, person.name and person.sister.name are ignored.

Note if you ignore specific field, all sub fields are ignored too, for example if person is ignored then person.name, person.address, person.address.number are ignored too.

Nice to have: report all the fields ignored in the comparison

status

  • ignore all null fields
  • ignore fields specified with a full path
  • fail if an ignored fields specified with a full path does not match any field
  • ignore fields specified with a regex
  • report the fields configured to be ignored in the comparison
  • report all the fields actually ignored in the comparison (to show which fields the regex matched)
  • documentation

Allow to use overridden equals method or not

By default the recursive comparison will use equals if it had been overridden but users should be able to specify to ignore overridden equals methods and compare objects field by field.

This can be done:

  • for all types matching given regexes
  • a specific given list of types with forcingRecursiveComparisonFor(Child.class, Sister.class)
  • a specific given list of fields with forcingRecursiveComparisonForFields("child", "sister") Nice to have

Example:

assertThat(person).usingRecursiveComparison()
                  .forcingRecursiveComparisonFor(Child.class, Sister.class)
                  .forcingRecursiveComparisonForFields("name", "city")
                  .isEqualTo(expectedPerson);

status

  • disable use of overridden equals method for types matching given regexes
  • disable use of overridden equals method for given types
  • disable use of overridden equals method for given fields
  • disable use of overridden equals method for all fields
  • documentation

Specify a comparison strategy per type or fields

Users should be able to specify a comparator for a given type or fields:

  • usingComparatorForType(String.class, caseInsensitiveComparator) for all types
  • usingComparatorForFields(caseInsensitiveComparator, "name", "city") a specific given list of fields (vararg)

To keep the API simple once a comparator for a given type is registered it should be used at any level, collection element or collection element field.

Field comparators take precedence over type ones as they are more fine grained.

Once a comparator is registered for a type or a field, it replaces the recursive comparison when these types/fields are being compared.

status

  • enable a comparison strategy per type
  • enable a comparison strategy per fields
  • documentation

Strict/lenient type checking

This will specify if two instances with the same fields but from different classes are considered equals, it allows for example to compare a Person with a PersonDto. If the check is strict the expected object class must be compatible (i.e. extends) actuals, so if Employee inherits Person one can compare a person with an employee (but not an employee with a person. The check is performed on the root objects and their fields.

By default the check is lenient but it can be made strict to fail the comparison.

status

  • enable strict type checking
  • lenient type checking should allow comparing collections of different type (ex TreeSet vs HashSet).
  • documentation

Handle cycles

The recursive comparison should handle cycles, for example when a -> b -> c -> a.

status

  • more tests with cycles
  • documentation

Map support

Maps entries should be considered as fields.
See if it is possible and relevant to report all compared maps internal differences (size, missing elements, elements differences, order when order is consistent ...) instead just reporting that the maps differ.

status

On hold at the moment as it is not crystal what can be achieved reasonably.

  • map support
  • documentation

Iterable/array support

At the moment assertThat(Iterable/array) does not expose usingRecursiveComparison() which makes it cumbersome to test them field/field.

The comparison by default fails if we compare an ordered collection to an unordered one but this should be configurable

Nice to have: ignores order in comparison (by default elements are compared in order for ordered collection).
Nice to have: allow to compare ordered collections with unordered one. (this should be disabled by default)
Nice to have: allow to compare array with ordered collections like list. (this should be disabled by default)

Example:

assertThat(fellowshipOfTheRing).usingRecursiveComparison()
                               .contains(frodo, sam, merry, pippin, gandalf, 
                                         legolas, boromir, aragorn, gimli);

status

  • provide elements recursive comparison out of the box for iterable assertions
  • ignores order in comparison (by default elements are compared in order for ordered collection).
  • allow to compare ordered collections with unordered one. (this should be disabled by default)
  • allow to compare array with ordered collections like list. (this should be disabled by default)
  • documentation

Error reporting

A failure must report the recursive comparison specification:

  • comparators used per type
  • comparators used per fields
  • ignored fields
  • when overridden equals are used and not used

The failure should show the number of differences and describe them all.

A difference describes fields by their full path from the root object, the actual value, the expected one and the comparison used if the user specified one.
Values are represented with AssertJ standard representation or the one set by the user.

Actual and expected values type should be displayed when strict type checking is enable.

The differences reported must be ordered by path alphabetically.

Difference report example:

person.sister.name differs: 
- actual value  : "Sansa"
- expected value: "Arya"
- comparison was performed with caseInsensitiveComparator 

The path to field must support maps and adress #1303.

status

  • basic error reporting
  • error reporting : show comparators used
  • error reporting : list ignored fields
  • error reporting : show that actual's null field were ignored in the comparison
  • error reporting : show which overridden equals were used
  • error reporting : the path to field must support maps
  • error reporting : report the index of the indexes of ordered collection field element, ex: friends[1].number
  • error reporting : the differences reported must be ordered by path alphabetically

Globally configuring the recursive comparison behavior

To avoid repeating the same recursive configuration before each assertion, AssertJ should provide:

  • a global way to configure the default recursive comparison behavior
  • a way capture easily on the recursive comparison configuration in order to reuse it

Changing the default recursive comparison behavior before all tests

AssertJ will extend the mechanism used to configure Representation.

Capture the recursive comparison setting to reuse it

If users don't want to change the default behavior globally but on a smaller scope, it will possible to capture a recursive comparison specification and reuse it.

Example:

// default behavior
RecursiveComparisonSpecification recursiveComparisonSpecification =  new RecursiveComparisonSpecification();
recursiveComparisonSpecification.ignoreNullFields();
recursiveComparisonSpecification.forceRecursiveComparisonFor(Child.class, Sister.class);

assertThat(person).usingRecursiveComparison(recursiveComparisonSpecification)
                  .isEqualTo(expectedPerson);

assertThat(person2).usingRecursiveComparison(recursiveComparisonSpecification)
                   .isEqualTo(expectedPerson2);

The example above is equivalent to this where we have to repeat call to ignoringNullFields and forcingRecursiveComparisonFor for each assertions:

assertThat(person).usingRecursiveComparison()
                  .ignoringNullFields()
                  .forcingRecursiveComparisonFor(Child.class, Sister.class)
                  .isEqualTo(expectedPerson);

assertThat(person2).usingRecursiveComparison(recursiveComparisonSpecification)
                   .ignoringNullFields()
                   .forcingRecursiveComparisonFor(Child.class, Sister.class)
                   .isEqualTo(expectedPerson2);

status

  • provide a way to globally configure the recursive comparison behavior

Tests checklist

The recursive comparison specification must be transferred after calling methods that change the object under test: extracting, asString ...

status

  • test recursive comparison is propagated

Initial issue description

The current way comparing objects recursively is starting to pollute the api, the methods introduced for the recursive comparison don't apply for the other assertion cluttering the assertj api, ex usingComparatorForType.

I would like to introduce a separate API to use recursive comparison allowing to fine tune the comparison behavior.

API rough draft (please criticize it !)

import static RecursiveComparisonSpecification;

// ignoringNullFields() comes from RecursiveComparisonSpecification
assertThat(person).usingRecursiveComparison(ignoringNullFields()
                                           .ignoringFields("name", "age")                                                 
                                           .forcingRecursiveComparisonFor(Child.class, Sister.class))
                  .isEqualTo(expectedPersont)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions