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” .
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()
}