diff --git a/examples/bankaccount-jgiven-junit5/src/test/kotlin/SagaStateAssertionTest.kt b/examples/bankaccount-jgiven-junit5/src/test/kotlin/SagaStateAssertionTest.kt new file mode 100644 index 0000000..b81e031 --- /dev/null +++ b/examples/bankaccount-jgiven-junit5/src/test/kotlin/SagaStateAssertionTest.kt @@ -0,0 +1,110 @@ +package io.holixon.axon.testing.examples.jgiven.junit5.kotlin + +import com.tngtech.jgiven.annotation.ProvidedScenarioState +import io.holixon.axon.testing.examples.jgiven.junit5.kotlin.SagaStateAssertionTest.StatefulSaga +import io.holixon.axon.testing.examples.jgiven.junit5.kotlin.SagaStateAssertionTest.StatefulSaga.Companion.associationProperty +import io.holixon.axon.testing.jgiven.AxonJGiven +import io.holixon.axon.testing.jgiven.junit5.SagaFixtureScenarioTest +import io.toolisticon.testing.jgiven.AND +import io.toolisticon.testing.jgiven.GIVEN +import io.toolisticon.testing.jgiven.THEN +import io.toolisticon.testing.jgiven.WHEN +import org.assertj.core.api.Assertions.assertThat +import org.axonframework.modelling.saga.AssociationValue +import org.axonframework.modelling.saga.EndSaga +import org.axonframework.modelling.saga.SagaEventHandler +import org.axonframework.modelling.saga.StartSaga +import org.junit.jupiter.api.Test + +internal class SagaStateAssertionTest : SagaFixtureScenarioTest() { + + data class StartSagaEvent(val key: String) + data class IncrementSagaEvent(val key: String, val add: Int) + data class EndSagaEvent(val key: String) + + class StatefulSaga { + companion object { + const val ASSOCIATION_PROPERTY = "key" + + fun associationProperty(key: String) = AssociationValue(ASSOCIATION_PROPERTY, key) + } + + lateinit var key: String + var state: Int = 0 + + @SagaEventHandler(associationProperty = ASSOCIATION_PROPERTY) + @StartSaga + fun on(evt: StartSagaEvent) { + this.key = evt.key + } + + @SagaEventHandler(associationProperty = ASSOCIATION_PROPERTY) + fun on(evt: IncrementSagaEvent) { + this.state += evt.add + } + + @SagaEventHandler(associationProperty = ASSOCIATION_PROPERTY) + @EndSaga + fun on(evt: EndSagaEvent) { + this.key = evt.key + } + + override fun toString(): String { + return "StatefulSaga(key='$key', state=$state)" + } + } + + @ProvidedScenarioState + private val fixture = AxonJGiven.sagaTestFixtureBuilder().build() + + @Test + fun `a fresh saga has state 0`() { + val key = "123" + + GIVEN.noPriorActivity() + + WHEN + .publishing(StartSagaEvent(key)) + + THEN + .expectActiveSagas(1) + .AND + .expectSagaState(associationProperty(key), "state is zero") { + assertThat(it.state).isEqualTo(0) + } + } + + + @Test + fun `saga state can be increased`() { + val key = "234" + + GIVEN.aggregatePublishedEvent(key, StartSagaEvent(key)) + + WHEN + .publishing(IncrementSagaEvent(key, 2)) + + THEN + .expectActiveSagas(1) + .AND + .expectSagaState(associationProperty(key), "state is two") { + assertThat(it.state).isEqualTo(2) + } + } + + @Test + fun `saga can be ended`() { + val key = "234" + + GIVEN + .aggregatePublishedEvent(key, StartSagaEvent(key)) + .AND + .aggregatePublishedEvent(key, IncrementSagaEvent(key, 2)) + + WHEN + .publishing(EndSagaEvent(key)) + + THEN + .expectActiveSagas(0) + } +} diff --git a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureGiven.kt b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureGiven.kt index d6bdb47..e50ac14 100644 --- a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureGiven.kt +++ b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureGiven.kt @@ -5,7 +5,9 @@ package io.holixon.axon.testing.jgiven.saga import com.tngtech.jgiven.Stage import com.tngtech.jgiven.annotation.* import io.holixon.axon.testing.jgiven.AxonJGivenStage +import org.axonframework.modelling.saga.repository.inmemory.InMemorySagaStore import org.axonframework.test.saga.FixtureExecutionResult +import org.axonframework.test.saga.FixtureExecutionResultImpl import org.axonframework.test.saga.SagaTestFixture import org.axonframework.test.saga.WhenState @@ -25,12 +27,18 @@ class SagaFixtureGiven : Stage>() { @ProvidedScenarioState private lateinit var thenState: FixtureExecutionResult + @ProvidedScenarioState + private lateinit var sagaStore: InMemorySagaStore + + @ProvidedScenarioState + private lateinit var sagaType: Class + + @BeforeStage internal fun init() { - with(SagaTestFixture::class.java.getDeclaredField("fixtureExecutionResult")) { - isAccessible = true - thenState = get(fixture) as FixtureExecutionResult - } + thenState = fixture.fixtureExecutionResult + sagaStore = fixture.sagaStore + sagaType = fixture.sagaType } /** diff --git a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureThen.kt b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureThen.kt index 0751d59..741d33b 100644 --- a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureThen.kt +++ b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureThen.kt @@ -5,21 +5,29 @@ package io.holixon.axon.testing.jgiven.saga import com.tngtech.jgiven.Stage import com.tngtech.jgiven.annotation.As import com.tngtech.jgiven.annotation.ExpectedScenarioState +import com.tngtech.jgiven.annotation.Hidden import io.holixon.axon.testing.jgiven.AxonJGivenStage import io.holixon.axon.testing.jgiven.step import org.axonframework.deadline.DeadlineMessage import org.axonframework.eventhandling.EventMessage +import org.axonframework.modelling.saga.AssociationValue +import org.axonframework.modelling.saga.repository.inmemory.InMemorySagaStore import org.axonframework.test.saga.FixtureExecutionResult import org.hamcrest.Matcher import java.time.Duration import java.time.Instant - @AxonJGivenStage class SagaFixtureThen : Stage>() { @ExpectedScenarioState(required = true) - lateinit var thenState: FixtureExecutionResult + private lateinit var thenState: FixtureExecutionResult + + @ExpectedScenarioState(required = true) + private lateinit var sagaStore: InMemorySagaStore + + @ExpectedScenarioState(required = true) + private lateinit var sagaType: Class /** * Expect the given number of Sagas to be active (i.e. ready to respond to incoming events. @@ -296,4 +304,25 @@ class SagaFixtureThen : Stage>() { fun expectDeadlinesMet(vararg expected: Any): SagaFixtureThen = step { thenState.expectTriggeredDeadlines(*expected) } + + + @As("expect saga state: \$description") + fun expectSagaState( + @Hidden associationValue: AssociationValue, + description: String, + @Hidden assertion: (T) -> Unit + ): SagaFixtureThen = step { + assertion(loadSaga(associationValue)) + } + + private fun loadSaga(associationValue: AssociationValue): T { + val sagas: List = sagaStore.findSagas(sagaType, associationValue) + .map { sagaKey -> + sagaStore.loadSaga(sagaType, sagaKey) + }.map { entry -> entry.saga() } + + check(sagas.size == 1) { "Expected exactly one saga for associationProperty=${associationValue.key}, value=${associationValue.value}, but found: ${sagas.size}." } + + return sagas.single() + } } diff --git a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureWhen.kt b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureWhen.kt index 9547e0a..32fd114 100644 --- a/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureWhen.kt +++ b/extension/jgiven/core/src/main/kotlin/saga/SagaFixtureWhen.kt @@ -1,4 +1,5 @@ @file:Suppress("unused") + package io.holixon.axon.testing.jgiven.saga import com.tngtech.jgiven.Stage @@ -12,7 +13,6 @@ import org.axonframework.test.saga.WhenState import java.time.Duration import java.time.Instant - /** * When stage for saga fixture. * @param T aggregate type. diff --git a/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt b/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt new file mode 100644 index 0000000..f304112 --- /dev/null +++ b/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt @@ -0,0 +1,31 @@ +package io.holixon.axon.testing.jgiven.saga + +import io.holixon.axon.testing.jgiven.saga.SagaTestFixtureFields.fieldFixtureFixtureExecutionResult +import io.holixon.axon.testing.jgiven.saga.SagaTestFixtureFields.fieldSagaStore +import io.holixon.axon.testing.jgiven.saga.SagaTestFixtureFields.fieldSagaType +import org.axonframework.modelling.saga.repository.inmemory.InMemorySagaStore +import org.axonframework.test.saga.FixtureExecutionResult +import org.axonframework.test.saga.SagaTestFixture +import java.lang.reflect.Field + +/** + * Handles access to private final fields in axon-test. + * Might become obsolete when fields are configurable in core. + */ +object SagaTestFixtureFields { + private val CLASS = SagaTestFixture::class.java + + private fun Class<*>.getDeclaredAccessibleField(fieldName: String): Field = getDeclaredField(fieldName) + .apply { + isAccessible = true + } + + val fieldSagaStore: Field = CLASS.getDeclaredAccessibleField("sagaStore") + val fieldFixtureFixtureExecutionResult: Field = CLASS.getDeclaredAccessibleField("fixtureExecutionResult") + val fieldSagaType: Field = CLASS.getDeclaredAccessibleField("sagaType") +} + +@Suppress("UNCHECKED_CAST") +val SagaTestFixture.sagaType: Class get() = fieldSagaType.get(this) as Class +val SagaTestFixture.fixtureExecutionResult: FixtureExecutionResult get() = fieldFixtureFixtureExecutionResult.get(this) as FixtureExecutionResult +val SagaTestFixture.sagaStore: InMemorySagaStore get() = fieldSagaStore.get(this) as InMemorySagaStore