Axon test fails when an aggregate field type is Set -> HashSet

I fell down in a weird (at least for me) failure when using Set instead List to hold a collection of objects.

I was able to reproduce the issue in this public repo so that I hope might be helpful to give a real case to help understand better.

For some reason the hash table of my hashset differs among test lifecycle, I mean event by event being applied when a command comes my collection of Schedules differs although the values are the same.

The test result is:


org.axonframework.test.AxonAssertionError: Illegal state change detected! Property "com.acme.axonsample.axonsample.WorkDay" has different value when sourcing events. Working aggregate value: <WorkDay(id=workDay-for-2020-01-02, day=2020-01-02, schedules=[Schedule(id=5dca3df439881c002543876e, workDuration=PT41M, begin=2020-01-02T10:00Z, end=2020-01-02T11:00Z, workLogs=Optional[[WorkLog(start=2020-01-02T09:45Z, stop=2020-01-02T10:35Z), WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z)]]), Schedule(id=7dca3bf439881c002543836b, workDuration=PT1H, begin=2020-01-02T12:00Z, end=2020-01-02T13:00Z, workLogs=Optional[[WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z)]]), Schedule(id=2dca3be439871c022543836a, workDuration=PT45M, begin=2020-01-02T14:00Z, end=2020-01-02T14:45Z, workLogs=Optional[[WorkLog(start=2020-01-02T13:58Z, stop=2020-01-02T14:45Z)]])], workLogs=[WorkLog(start=2020-01-02T09:45Z, stop=2020-01-02T10:35Z), WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z), WorkLog(start=2020-01-02T13:58Z, stop=2020-01-02T14:45Z)])> Value after applying events: <WorkDay(id=workDay-for-2020-01-02, day=2020-01-02, schedules=[Schedule(id=7dca3bf439881c002543836b, workDuration=PT1H, begin=2020-01-02T12:00Z, end=2020-01-02T13:00Z, workLogs=Optional[[WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z)]]), Schedule(id=2dca3be439871c022543836a, workDuration=PT45M, begin=2020-01-02T14:00Z, end=2020-01-02T14:45Z, workLogs=Optional[[WorkLog(start=2020-01-02T13:58Z, stop=2020-01-02T14:45Z)]]), Schedule(id=5dca3df439881c002543876e, workDuration=PT41M, begin=2020-01-02T10:00Z, end=2020-01-02T11:00Z, workLogs=Optional[[WorkLog(start=2020-01-02T09:45Z, stop=2020-01-02T10:35Z), WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z)]])], workLogs=[WorkLog(start=2020-01-02T09:45Z, stop=2020-01-02T10:35Z), WorkLog(start=2020-01-02T10:54Z, stop=2020-01-02T13:00Z), WorkLog(start=2020-01-02T13:58Z, stop=2020-01-02T14:45Z)])>

I didn’t find on documentation any restrictions to use Set even though I got stuck with that failure. Set is important to me in order to guarantee no duplications

The order of the schedules property is different, so they are in fact not equal assuming schedules is an ordered list. Axon tests win again!

Hum… interesting, if the order is a constraint for testing Set collections should be avoided or never used?

In that case a Set doesn’t guarantee a insertion order but is important to avoid duplications, seems it should never being used since tests expect an ordered collection of elements.

Look I am not saying ordered events being handled but ordered elements of type Schedule within an aggregate.

In the repo I have provided an implementation using Set and a branch with a “fix” using List.

So the final verdict is: Use List and avoid Set?

-Rogério

Unfortunately the “tests win again”, but my application fails without an advise on documentation.

My apologies, I didn’t realize that was the set in question. I’ve found that the tests are just using basic equality under the hood, I’d double check that the underlying objects are behaving correctly.

I have debugging deeply and I realize the hash table KEY end up different in when equals is called.

I had override the equals method instead use AbstractCollections.containsAll I had implemented a hack method that compares a Set of Schedules against all elements comparing value by value and it works, but I am not comfortable with that solution it sounds like a wrong direction to go.

Since all works fine using List I expected to have the same using Set because the underlying objects behaviour doesn’t change as one can realize on my axonsample repo.

Or in the end would be helpful to know that Set collections should be avoided.

regards,

-Rogério

Hi Rogério, Joel,

Your bumping into the side-effect validation of the test fixtures here. It helps make sure you correctly implemented Event Sourcing. What is does is, after executing your command, load a second aggregate based on the events produced by the commands. It then checks whether the two are equal.

The test fixtures use a special deep-equals method to check for equality of Aggregate state. Basically, it checks all the equality of the fields. As long as the values in the fields don’t override Object’s equals() method, it uses field equality. The issue here is that Java’s collection implementations all override equals(). Their implementation then checks for equals methods on your entity objects.

You can switch this mechanism off (at your own risk) by calling setReportIllegalStateChange(false) on the fixture.

Cheers,

Hi Allard!

TBH I prefer change my implementation to use List and avoid duplications in the code than switch off a test validation. I used to test application to avoid risks not to take them.

Thanks for your time on this issue,

Best

-Rogério

Hi Rogerio,

you’re absolutely correct. Do note, however, that if (Hash)Set doesn’t work and List does, this may be an indication that you failed to implement the hashCode method correctly. For these collections, Axon lets the collection verify equality. All the ‘hash***’ collections rely on hashCode being implemented.

Cheers,

Allard

I have provided a sample code to try to bring clarification about how I have implemented and faced that issue, it can be found here .

I am using Lombok @EqualsAndHashCode in addition I also tried IntelliJ generators like IntelliJDefault and Java7+ approaches, in all cases I end up in the same error.

I hardly try to get rid of this, but no success on my attempts.

Best,

-Rogério

Hi Rogerio,

I have managed to trace this issue down. Long story short: your equals and hashCode contracts are violating the requirements of HashMap.
Short story long:
Your schedules are added to a HashMap. The hashmap uses buckets based on the hashcode of an object to place these objects. If the hashcode changes, the implementation may start looking into another bucket to find an item and cannot find it there, even if (strictly speaking) the object is in the map.

In general: always make sure the hashcode is exclusively based on immutable fields. It seems that Lombok makes it very easy and convenient to violate this rule :-/.

Solution:
Don’t annotate your aggregate with @Data (Aggregates actually aren’t data classes). @Getter makes it compile, but I wouldn’t recommend depending on getters in this case.
On schedule, implement equals and hashcode explicitly. I haven’t figured out why, but it seems like Lombok creates a mess out of the equals method. It just won’t work.

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Schedule schedule = (Schedule) o;
return id.equals(schedule.id) &&
workDuration.equals(schedule.workDuration) &&
begin.equals(schedule.begin) &&
end.equals(schedule.end) &&
workLogs.equals(schedule.workLogs);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

Cheers,

Hi Allard!

First of all, thank you so much for the time spent on this issue.

I tried different approaches defining equals and hashCode not only Lombok, although I agree with you Lombok hides a lot of trash in the code behind the scene, but is out of my hand to remove it.

Sounds that the trick part is to define a hashCode just using ID not all fields, even though the fields values never changes.

I am fine with you last answer and time to solve this question. I will try your approach and align with my team if we will try it to keep bonded on HashSets or just keep using the List interface as our default collection.

My best regards,

-Rogério