Skip to content

Commit

Permalink
allow state assertions on sagas
Browse files Browse the repository at this point in the history
fixes #333
  • Loading branch information
jangalinski committed Sep 21, 2023
1 parent f5fa73a commit aac244b
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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<StatefulSaga>() {

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<StatefulSaga>().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)
}
}
16 changes: 12 additions & 4 deletions extension/jgiven/core/src/main/kotlin/saga/SagaFixtureGiven.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,12 +27,18 @@ class SagaFixtureGiven<T> : Stage<SagaFixtureGiven<T>>() {
@ProvidedScenarioState
private lateinit var thenState: FixtureExecutionResult

@ProvidedScenarioState
private lateinit var sagaStore: InMemorySagaStore

@ProvidedScenarioState
private lateinit var sagaType: Class<T>


@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
}

/**
Expand Down
33 changes: 31 additions & 2 deletions extension/jgiven/core/src/main/kotlin/saga/SagaFixtureThen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : Stage<SagaFixtureThen<T>>() {

@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<T>

/**
* Expect the given number of Sagas to be active (i.e. ready to respond to incoming events.
Expand Down Expand Up @@ -296,4 +304,25 @@ class SagaFixtureThen<T> : Stage<SagaFixtureThen<T>>() {
fun expectDeadlinesMet(vararg expected: Any): SagaFixtureThen<T> = step {
thenState.expectTriggeredDeadlines(*expected)
}


@As("expect saga state: \$description")
fun expectSagaState(
@Hidden associationValue: AssociationValue,
description: String,
@Hidden assertion: (T) -> Unit
): SagaFixtureThen<T> = step {
assertion(loadSaga(associationValue))
}

private fun loadSaga(associationValue: AssociationValue): T {
val sagas: List<T> = 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()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:Suppress("unused")

package io.holixon.axon.testing.jgiven.saga

import com.tngtech.jgiven.Stage
Expand All @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions extension/jgiven/core/src/main/kotlin/saga/_reflection.kt
Original file line number Diff line number Diff line change
@@ -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 <T> SagaTestFixture<T>.sagaType: Class<T> get() = fieldSagaType.get(this) as Class<T>
val <T> SagaTestFixture<T>.fixtureExecutionResult: FixtureExecutionResult get() = fieldFixtureFixtureExecutionResult.get(this) as FixtureExecutionResult
val <T> SagaTestFixture<T>.sagaStore: InMemorySagaStore get() = fieldSagaStore.get(this) as InMemorySagaStore

0 comments on commit aac244b

Please sign in to comment.