AggregateTestFixture with event received from different test method in same class

Hi,

While writing tests with AggregateTestFixture, I got an issue where an event that is published from the execution of a command in the when method is also applied in another test method which it should not, and so the test fails.
To verify that I was receiving the event from “test 2” command, I used the debugger and it was processed while the “test 1” was running.

I tried to write an example below, but cannot reproduce the issue when not using my original code but in my original code, when “test 2” runs before “test 1” then I get the event from “test 2” in the “test 1”, and if we invert test execution then all tests pass.
The event that I receive in “test 1” is the event DoOnEntityEvent published by the command of “test 2”.

So I wonder if there is anything wrong with using “static” id that never change, my aggregate root is kind of a global configuration so there is only one, then it has Map<*, *> instances where the id is also known in advance.

I even tried to create the AggregateTestFixture in each local test method and still get the event from “test 2” in “test 1” :face_with_raised_eyebrow:.

And then, “test 3” (in my original code) will not trigger any event (test will fail) as apparently, the aggregate already has the state as what “test 3” command is trying to change to → So it means that the event from “test 1” is applied in “test 3”.

package org.blackdread.test.axon

import org.axonframework.commandhandling.CommandHandler
import org.axonframework.eventsourcing.EventSourcingHandler
import org.axonframework.extensions.kotlin.applyEvent
import org.axonframework.modelling.command.*
import org.axonframework.spring.stereotype.Aggregate
import org.axonframework.test.aggregate.AggregateTestFixture
import org.axonframework.test.aggregate.FixtureConfiguration
import org.axonframework.test.matchers.Matchers.*
import org.blackdread.test.axon.ExampleFailEntityTest.BaseId.Companion.baseId
import org.blackdread.test.axon.ExampleFailEntityTest.EntityExampleId.Companion.entityExampleId
import org.blackdread.test.axon.ExampleFailEntityTest.EntityExampleType.TYPE_1
import org.blackdread.test.axon.ExampleFailEntityTest.EntityExampleType.TYPE_2
import org.junit.jupiter.api.*
import java.util.*
import java.util.UUID.randomUUID

@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
internal class ExampleFailEntityTest {

    private lateinit var fixture: FixtureConfiguration<BaseAggregate>

    @BeforeEach
    fun setUp() {
        fixture = AggregateTestFixture(BaseAggregate::class.java)
    }

    @Order(2)
    @Test
    fun `test 1`() {
        val evt1 = aBaseCreatedEvent()
        val evt2 = aBaseEntityAddedEvent()
        val evt3 = anAddLevelEvent()

        val cmd = aDoOnLevelCmd(levelId = evt3.levelId)

        fixture.given(evt1, evt2, evt3)
            .`when`(cmd)
            .expectNotMarkedDeleted()
            .expectSuccessfulHandlerExecution()
            .expectNoScheduledDeadlines()
            .expectEventsMatching(
                payloadsMatching(
                    exactSequenceOf(
                        deepEquals(
                            DoOnLevelEvent(
                                entityExampleType = cmd.entityExampleType,
                                levelId = cmd.levelId,
                                previous = emptyMap(),
                                current = cmd.someValue,
                            )
                        ),
                        andNoMore()
                    )
                )
            )
    }

    @Order(1)
    @Test
    fun `test 2`() {
        val evt1 = aBaseCreatedEvent()
        val evt2 = aBaseEntityAddedEvent()
        val evt3 = anAddLevelEvent()
        val evt4 = aDoOnLevelEvent(levelId = evt3.levelId)

        val cmd = aDoOnLevelCmd(levelId = evt3.levelId, someValue = mapOf("key1other" to "value1other"))

        fixture.given(evt1, evt2, evt3, evt4)
            .`when`(cmd)
            .expectNotMarkedDeleted()
            .expectSuccessfulHandlerExecution()
            .expectNoScheduledDeadlines()
            .expectEventsMatching(
                payloadsMatching(
                    exactSequenceOf(
                        deepEquals(
                            DoOnLevelEvent(
                                entityExampleType = cmd.entityExampleType,
                                levelId = cmd.levelId,
                                previous = evt4.current,
                                current = cmd.someValue,
                            )
                        ),
                        andNoMore()
                    )
                )
            )
    }

    @Order(3)
    @Test
    fun `test 3`() {
        val evt1 = aBaseCreatedEvent()
        val evt2 = aBaseEntityAddedEvent()
        val evt3 = anAddLevelEvent()

        val cmd = aDoOnLevelCmd(levelId = evt3.levelId)

        fixture.given(evt1, evt2, evt3)
            .`when`(cmd)
            .expectNotMarkedDeleted()
            .expectSuccessfulHandlerExecution()
            .expectNoScheduledDeadlines()
            .expectEventsMatching(
                payloadsMatching(
                    exactSequenceOf(
                        deepEquals(
                            DoOnLevelEvent(
                                entityExampleType = cmd.entityExampleType,
                                levelId = cmd.levelId,
                                previous = emptyMap(),
                                current = cmd.someValue,
                            )
                        ),
                        andNoMore()
                    )
                )
            )
    }

    @Order(4)
    @Test
    fun `test 4`() {
        val evt1 = aBaseCreatedEvent()
        val evt2 = aBaseEntityAddedEvent()
        val evt3 = anAddLevelEvent()

        val evt4 = aBaseEntityAddedEvent(TYPE_2)
        val evt5 = anAddLevelEvent(TYPE_2)

        val cmd = aDoOnLevelCmd(TYPE_2, levelId = evt5.levelId, someValue = mapOf("key1other" to "value1other"))

        fixture.given(evt1, evt2, evt3, evt4, evt5)
            .`when`(cmd)
            .expectNotMarkedDeleted()
            .expectSuccessfulHandlerExecution()
            .expectNoScheduledDeadlines()
            .expectEventsMatching(
                payloadsMatching(
                    exactSequenceOf(
                        deepEquals(
                            DoOnLevelEvent(
                                entityExampleType = cmd.entityExampleType,
                                levelId = cmd.levelId,
                                previous = emptyMap(),
                                current = cmd.someValue,
                            )
                        ),
                        andNoMore()
                    )
                )
            )
    }


    @Suppress("DataClassPrivateConstructor")
    data class BaseId private constructor(val identifier: String) {

        companion object {

            private val SINGLETON_ID = BaseId("settings_id1")
            fun baseId(): BaseId = SINGLETON_ID
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is BaseId) return false

            if (identifier != other.identifier) return false

            return true
        }

        override fun hashCode(): Int {
            return identifier.hashCode()
        }

    }

    @Suppress("DataClassPrivateConstructor")
    data class EntityExampleId private constructor(val identifier: String) {

        companion object {
            @JvmStatic
            private val ID_MAP: EnumMap<EntityExampleType, EntityExampleId> = EnumMap(
                mapOf(
                    TYPE_1 to EntityExampleId("building_id1"),
                    TYPE_2 to EntityExampleId("building_id2"),
                )
            )

            fun entityExampleId(buildingType: EntityExampleType): EntityExampleId = ID_MAP.getValue(buildingType)
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is EntityExampleId) return false

            if (identifier != other.identifier) return false

            return true
        }

        override fun hashCode(): Int {
            return identifier.hashCode()
        }

    }

    private fun aBaseCreatedEvent() = BaseCreatedEvent("notUsed")
    private fun aBaseEntityAddedEvent(
        entityExampleType: EntityExampleType = TYPE_1,
    ) = BaseEntityAddedEvent(entityExampleType)

    private fun anAddLevelCmd(
        entityExampleType: EntityExampleType = TYPE_1,
        levelId: UUID = randomUUID(),
    ) = AddLevelCmd(entityExampleType, levelId)

    private fun anAddLevelEvent(
        entityExampleType: EntityExampleType = TYPE_1,
        levelId: UUID = randomUUID(),
    ) = AddLevelEvent(entityExampleType, levelId)

    private fun aDoOnLevelCmd(
        entityExampleType: EntityExampleType = TYPE_1,
        levelId: UUID = randomUUID(),
        someValue: Map<String, String> = mapOf("key1" to "value1"),
    ) = DoOnLevelCmd(entityExampleType, levelId, someValue)

    private fun aDoOnLevelEvent(
        entityExampleType: EntityExampleType = TYPE_1,
        levelId: UUID = randomUUID(),
        previous: Map<String, String> = emptyMap(),
        current: Map<String, String> = mapOf("key1" to "value1"),
    ) = DoOnLevelEvent(entityExampleType, levelId, previous, current)


    @Aggregate
    class BaseAggregate {

        @AggregateIdentifier
        private lateinit var baseId: BaseId

        @AggregateMember(eventForwardingMode = ForwardMatchingInstances::class)
        val entityExamples = EnumMap<EntityExampleType, EntityExample>(EntityExampleType::class.java)

        private constructor() {
            // Required by Axon Framework
        }

        @CommandHandler
        private constructor(cmd: BaseCreateCmd) {
            applyEvent(BaseCreatedEvent(cmd.unusedHere))
        }

        @EventSourcingHandler
        private fun on(evt: BaseCreatedEvent) {
            baseId = evt.baseId
        }

        @CommandHandler
        private fun handle(cmd: BaseEntityAddCmd) {
            applyEvent(
                BaseEntityAddedEvent(cmd.entityExampleType)
            )
        }

        @EventSourcingHandler
        private fun on(evt: BaseEntityAddedEvent) {
            entityExamples[evt.entityExampleType] =
                EntityExample(evt.entityExampleId, evt.entityExampleType, this)
        }


    }

    class EntityExample(
        private val entityExampleId: EntityExampleId,
        @EntityId
        private val entityExampleType: EntityExampleType,
        private val parent: BaseAggregate,
    ) {

        private val levels = mutableMapOf<UUID, SomeLevel>()

        @CommandHandler
        private fun handle(cmd: AddLevelCmd) {
            println("this in cmd $entityExampleType: $this")
            if (levels.containsKey(cmd.levelId)) throw IllegalStateException("Level already exist")

            applyEvent(
                AddLevelEvent(
                    entityExampleType = cmd.entityExampleType,
                    levelId = cmd.levelId,
                )
            )
        }

        @EventSourcingHandler
        private fun on(evt: AddLevelEvent) {
            println("this in event $entityExampleType: $this")
            levels[evt.levelId] = SomeLevel(entityExampleId, entityExampleType, evt.levelId)
        }

        @CommandHandler
        private fun handle(cmd: DoOnLevelCmd) {
            val someLevel = levels.getValue(cmd.levelId)
            val previous = someLevel.someValue.toMap()

            if (previous == cmd.someValue) return

            applyEvent(
                DoOnLevelEvent(cmd.entityExampleType, cmd.levelId, previous, cmd.someValue)
            )
        }

        @EventSourcingHandler
        private fun on(evt: DoOnLevelEvent) {
            val someLevel = levels.getValue(evt.levelId)
            someLevel.someValue.clear()
            someLevel.someValue.putAll(evt.current)
        }


        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is EntityExample) return false

            if (entityExampleType != other.entityExampleType) return false

            return true
        }

        override fun hashCode(): Int {
            return entityExampleType.hashCode()
        }

        class SomeLevel(
            val entityExampleId: EntityExampleId,
            val entityExampleType: EntityExampleType,
            val levelId: UUID,
        ) {

            val someValue: MutableMap<String, String> = mutableMapOf()

            override fun equals(other: Any?): Boolean {
                if (this === other) return true
                if (other !is SomeLevel) return false

                if (entityExampleId != other.entityExampleId) return false
                if (entityExampleType != other.entityExampleType) return false
                if (levelId != other.levelId) return false

                return true
            }

            override fun hashCode(): Int {
                var result = entityExampleId.hashCode()
                result = 31 * result + entityExampleType.hashCode()
                result = 31 * result + levelId.hashCode()
                return result
            }
        }

    }

    enum class EntityExampleType {
        TYPE_1,
        TYPE_2;

        val id: EntityExampleId
            get() = entityExampleId(this)
    }

    data class BaseCreateCmd(
        val unusedHere: String,
    ) {
        @TargetAggregateIdentifier
        val baseId: BaseId = baseId()
    }

    data class BaseCreatedEvent(
        val unusedHere: String,
    ) {
        val baseId: BaseId = baseId()
    }

    // =======

    abstract class BaseEntityExampleCmd {

        @TargetAggregateIdentifier
        val baseId: BaseId = baseId()

        val entityExampleId: EntityExampleId
            get() = entityExampleType.id

        abstract val entityExampleType: EntityExampleType

    }

    abstract class BaseEntityExampleEvent {

        val baseId: BaseId = baseId()

        val entityExampleId: EntityExampleId
            get() = entityExampleType.id

        abstract val entityExampleType: EntityExampleType

    }

    data class BaseEntityAddCmd(
        override val entityExampleType: EntityExampleType,
    ) : BaseEntityExampleCmd()

    data class BaseEntityAddedEvent(
        override val entityExampleType: EntityExampleType,
    ) : BaseEntityExampleEvent()

    // =======

    abstract class BaseEntityExampleLevelCmd : BaseEntityExampleCmd() {

        abstract val levelId: UUID

    }

    abstract class BaseEntityExampleLevelEvent : BaseEntityExampleEvent() {

        abstract val levelId: UUID

    }

    data class AddLevelCmd(
        override val entityExampleType: EntityExampleType,
        override val levelId: UUID,
    ) : BaseEntityExampleLevelCmd()

    data class AddLevelEvent(
        override val entityExampleType: EntityExampleType,
        override val levelId: UUID,
    ) : BaseEntityExampleLevelEvent()

    // =======

    data class DoOnLevelCmd(
        override val entityExampleType: EntityExampleType,
        override val levelId: UUID,
        val someValue: Map<String, String>,
    ) : BaseEntityExampleLevelCmd()

    data class DoOnLevelEvent(
        override val entityExampleType: EntityExampleType,
        override val levelId: UUID,
        val previous: Map<String, String>,
        val current: Map<String, String>,
    ) : BaseEntityExampleLevelEvent()

}

Hi Yoann,

I agree that’s pretty strange behavior. What AF version do you use? What do you mean by the static id that never changes?
I tried to run and analyze the code you provided and didn’t find anything suspicious. I’d appreciate it if you could share a snippet of code reproducing the issue.

Regards,
Michal

Hey, thanks for trying :grin:
I changed my original post code example, it looks much more like original code.
Still cannot get same error as my original code.
Maybe I passed a reference to a mutable object in my events but I cannot see it in my original code :smiling_face_with_tear: and I do not have any static mutable object either.

I am using axon 4.6.2 with dependencyManagement

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-bom</artifactId>
    <version>${axon.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

What I meant by static id is the val baseId: BaseId = baseId() in commands and events and same for EntityExampleId where the id is always the same between tests but that’s not supposed to be an issue.

The issue is very probably in the original code and not in axon framework, so I will keep investigating and will post here once I find the bug.

To explain better the issue:
When “test 1” runs, “test 2” already succeeded as I use the @Order annotation to make sure that “test 2” runs first.
So when DoOnLevelCmd is received in “test 1”, the val previous = someLevel.someValue.toMap() value will be the result of “test 2”, so I still cannot find how this is possible but somewhere a state must stay in memory or other, maybe a static (but I have none of that) or other.

Thank you for your time, I found the error and obviously this is on me.

The issue was I have strongly typed Map<SomeEnum, SomeImmutableDataClass> where I have two interfaces:

  • SomeImmutableDataClassMap : Map<SomeEnum, SomeImmutableDataClass>
  • MutableSomeImmutableDataClassMap : Map<SomeEnum, SomeImmutableDataClass>

Implementation for SomeImmutableDataClassMap is fully immutable and I have few factory methods to instantiate the implementation.

Implementation for MutableSomeImmutableDataClassMap is not immutable, so it allows to modify entries with extra methods defined in it. Again I have factory methods, which I copied from SomeImmutableDataClassMap which uses a singleton for the factory method empty() which is just an empty type map but for MutableSomeImmutableDataClassMap this is of course a mistake, it should return a new empty instance each time :melting_face: :melting_face:.

I already some similar type Map but had not the issue with those as when I implemented those, I had not copied the factory methods implementation but wrote it manually :smiling_face_with_tear:

I had written unit tests for SomeImmutableDataClassMap but not for MutableSomeImmutableDataClassMap :sweat_smile: