Comparing two objects with "special" assertions for certain fields with AssertJ
Asked Answered
S

3

8

Given the following class...

public class UserAccount {
  private Long id;
  private String email;
  private String activationCode;
  private Date createDate;
}

... I need to compare the actual object with an expected object using AssertJ. However the fields id, activationCode and createDate have dynamic value which I can't hard-code into the assertion.

So the following assertion would fail:
assertThat(actualUserAccount).isEqualTo(expectedUserAccount);

Then I found the following which would ignore certain fields:

assertThat(actualUserAccount)
        .usingRecursiveComparison()
        .ignoringFields("id", "activationCode", "createDate")
        .isEqualTo(expectedUserAccount);

But what I'm actually looking for is to assert for the objects being equal with the following special checks on certain fields:

  • Is there an id with any Long value?
  • Is there an activationCode with any String value?
  • Is there an createDate with any Date value?

Or is there no other way than to write one assertion for each field?

Summerlin answered 5/6, 2021 at 21:35 Comment(0)
L
12

You can specify how to compare certain fields with withEqualsForFields so you could write something like:

assertThat(actualUserAccount)
        .usingRecursiveComparison()
        .withEqualsForFields((id1, id2) -> id1 instanceof Long && id2 instanceof Long, "id")
        .isEqualTo(expectedUserAccount);

I'm checking both ids fields since there is no garantee that id1 is the actual id and id2 the expected id.

You could also write a generic method like BiPredicate<A, B> isType(T type) that returns a bipredicate checking both parameters are of the type T (my suggested signature might not work but you get the idea), that would let you write:

assertThat(actualUserAccount)
        .usingRecursiveComparison()
        .withEqualsForFields(isType(Long.class), "id")
        .withEqualsForFields(isType(String.class), "activationCode")
        .withEqualsForFields(isType(Date.class), "createDate")
        .isEqualTo(expectedUserAccount);

Here's what isType looks like (I haven't tested it though):

<A, B, T extends Class<?>> BiPredicate<A, B> isType(T type) {
    return (a, b) -> type.isInstance(a) && type.isInstance(b);
}

Having said that, I would probably not go that way and write additional assertions.

For reference: https://assertj.github.io/doc/#assertj-core-recursive-comparison-comparators

Lewse answered 7/6, 2021 at 21:51 Comment(0)
S
2

If you want to verify that all the fields have a value you could use hasNoNullFieldsOrProperties, while returns can be used to refine the verification of email (assuming that getters are exposed):

assertThat(actualUserAccount)
  .hasNoNullFieldsOrProperties()
  .returns(expectedUserAccount.getEmail(), from(UserAccount::getEmail));

If you must enforce the type of the fields, you can chain:

  .extracting(UserAccount::getId, UserAccount::getEmail, UserAccount::getActivationCode, UserAccount::getCreateDate)
  .hasExactlyElementsOfTypes(Long.class, String.class, String.class, Date.class);

If getters are not available, your original example is probably the only option to verify the value of email:

assertThat(actualUserAccount)
    .usingRecursiveComparison()
    .ignoringFields("id", "activationCode", "createDate")
    .isEqualTo(expectedUserAccount);

But still you can use extracting(String...) instead of extracting(Function...) to enforce the type of the fields:

assertThat(actualUserAccount)
  .extracting("id", "email", "activationCode", "createDate")
  .hasExactlyElementsOfTypes(Long.class, String.class, String.class, Date.class);

However, these options are not refactoring-friendly.

Stockstill answered 6/6, 2021 at 7:17 Comment(0)
A
1

This might not apply directly, because you were interested in any date value. But if you want to make sure that a date is "approximately exact", the use of withEqualsForType might be useful (the example uses OffsetDateTimes, but the logic is the same). A typical example would be that you want to check that the current timestamp will be set during the method you are testing, but you can't know in advance the precise moment it will happen. You can set "now" during the test and then assert that the actual timestamp will differ from it for a certain delta at max (for example one second). If, for example, you are writing timestamps in OffsetDateTimes (if you are using Long you will probably not be able to use this method), you can do this:

assertThat(actualObject)
    .usingRecursiveComparison()
    .withEqualsForType(timestampAlmostEquals, OffsetDateTime.class)
    .isEqualTo(expectedObject);

where

BiPredicate<OffsetDateTime, OffsetDateTime> timestampAlmostEquals = (t1, t2) -> Math.abs(ChronoUnit.SECONDS.between(t1, t2)) < 1;

All the OffsetDateTime fields in the actual and expected objects will be treated as equal as long as they do not differ by more than one second.

Again, it does not apply directly to you question, but when I was looking for a solution to my problem google search pointed me to yours. So maybe it will be useful to some other lost soul :D

Associative answered 13/4, 2023 at 18:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.