From b0a307ffb96f085be824cebdcb67135a3732f15f Mon Sep 17 00:00:00 2001 From: Jan Galinski Date: Tue, 21 Nov 2023 12:47:29 +0100 Subject: [PATCH] feat(364): provide CorrelationDataProviders and test for payload+meta (#365) fixes #364 --- extension/jgiven/core/pom.xml | 6 ++ .../jgiven/core/src/main/kotlin/AxonJGiven.kt | 24 ++++++ .../kotlin/aggregate/AggregateFixtureThen.kt | 28 ++++++- .../aggregate/AggregateTestFixtureBuilder.kt | 13 ++++ .../aggregate/AggregateTestFixtureContext.kt | 6 +- .../aggregate/AggregateTestFixtureExt.kt | 15 ---- .../src/main/kotlin/aggregate/_reflection.kt | 19 +++++ .../core/src/main/kotlin/saga/_reflection.kt | 6 +- .../src/test/kotlin/AxonJGivenTestFixtures.kt | 15 ++-- .../AggregateTestFixtureBuilderTest.kt | 74 +++++++++++++++++++ .../kotlin/aggregate/DummyAggregateTest.kt | 40 ---------- 11 files changed, 173 insertions(+), 73 deletions(-) delete mode 100644 extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureExt.kt create mode 100644 extension/jgiven/core/src/main/kotlin/aggregate/_reflection.kt create mode 100644 extension/jgiven/core/src/test/kotlin/aggregate/AggregateTestFixtureBuilderTest.kt delete mode 100644 extension/jgiven/core/src/test/kotlin/aggregate/DummyAggregateTest.kt diff --git a/extension/jgiven/core/pom.xml b/extension/jgiven/core/pom.xml index 233c84c..0cb3257 100644 --- a/extension/jgiven/core/pom.xml +++ b/extension/jgiven/core/pom.xml @@ -75,6 +75,12 @@ 1 test + + org.assertj + assertj-core + ${assertj.version} + test + ch.qos.logback logback-classic diff --git a/extension/jgiven/core/src/main/kotlin/AxonJGiven.kt b/extension/jgiven/core/src/main/kotlin/AxonJGiven.kt index 2577ec8..f38c8a4 100644 --- a/extension/jgiven/core/src/main/kotlin/AxonJGiven.kt +++ b/extension/jgiven/core/src/main/kotlin/AxonJGiven.kt @@ -9,6 +9,10 @@ import io.holixon.axon.testing.jgiven.saga.SagaFixtureGiven import io.holixon.axon.testing.jgiven.saga.SagaFixtureThen import io.holixon.axon.testing.jgiven.saga.SagaFixtureWhen import io.holixon.axon.testing.jgiven.saga.SagaTestFixtureBuilder +import org.axonframework.eventhandling.DomainEventMessage +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.function.Predicate /** * Base class for scenario aggregate tests. @@ -27,6 +31,26 @@ object AxonJGiven { inline fun aggregateTestFixtureBuilder() = AggregateTestFixtureBuilder(T::class.java) inline fun sagaTestFixtureBuilder() = SagaTestFixtureBuilder(T::class.java) + + internal fun Class<*>.getDeclaredAccessibleField(fieldName: String): Field = getDeclaredField(fieldName) + .apply { + isAccessible = true + } + + internal fun Class<*>.getDeclaredAccessibleMethod(methodName: String): Method = getDeclaredMethod(methodName) + .apply { + isAccessible = true + } + + fun listContainsEventPayloadAndMetaData(payload: Any, metaData: Map): Predicate>> = Predicate { list -> + val msgMeta = list.find { it.payload == payload }?.metaData ?: emptyMap() + + // and if so, does it contain all required metaData? + metaData.entries.map { (k, v) -> + msgMeta.containsKey(k) && v == msgMeta[k] + }.reduce { acc, b -> acc && b } + } + } /** diff --git a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateFixtureThen.kt b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateFixtureThen.kt index 269e3a4..6a273df 100644 --- a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateFixtureThen.kt +++ b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateFixtureThen.kt @@ -3,13 +3,19 @@ package io.holixon.axon.testing.jgiven.aggregate import com.tngtech.jgiven.Stage -import com.tngtech.jgiven.annotation.* +import com.tngtech.jgiven.annotation.As +import com.tngtech.jgiven.annotation.Hidden +import com.tngtech.jgiven.annotation.ProvidedScenarioState +import com.tngtech.jgiven.annotation.Quoted +import io.holixon.axon.testing.jgiven.AxonJGiven import io.holixon.axon.testing.jgiven.AxonJGivenStage import io.holixon.axon.testing.jgiven.step import org.axonframework.commandhandling.CommandResultMessage import org.axonframework.deadline.DeadlineMessage import org.axonframework.eventhandling.EventMessage +import org.axonframework.test.aggregate.Reporter import org.axonframework.test.aggregate.ResultValidator +import org.axonframework.test.matchers.Matchers import org.hamcrest.Matcher import org.hamcrest.MatcherAssert import java.time.Duration @@ -25,7 +31,9 @@ import kotlin.reflect.KClass class AggregateFixtureThen : Stage>() { @ProvidedScenarioState - private var context: AggregateTestFixtureContext = AggregateTestFixtureContext() + private var context: AggregateTestFixtureContext = AggregateTestFixtureContext() + + private val reporter = Reporter() /** * Expect event. @@ -34,6 +42,21 @@ class AggregateFixtureThen : Stage>() { @As("expect event:") fun expectEvent(event: Any) = this.expectEvents(event) + @As("expect event:\$ with metaData:\$") + fun expectEventWithMetaData(event: Any, vararg entries: Pair) = step { + val identifier = checkNotNull(context.aggregateIdentifier) { "no aggregateIdentifier present" } + + val events = context.fixture?.eventStore?.readEvents(identifier)?.asSequence()?.toList() ?: emptyList() + + val predicate = AxonJGiven.listContainsEventPayloadAndMetaData(event, mapOf(*entries)) + + val matcher = Matchers.predicate(predicate) + + if (!matcher.matches(events)) { + reporter.reportWrongResult(events, "Event exists with payload=$event and metaData=${mapOf(*entries)}.") + } + } + /** * Expect a series of events. * @param events events to expect. @@ -172,6 +195,7 @@ class AggregateFixtureThen : Stage>() { @As("expect result message") fun expectResultMessage(message: CommandResultMessage<*>) = execute { expectResultMessage(message) + } /** diff --git a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureBuilder.kt b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureBuilder.kt index 3f771a3..f3dfcc0 100644 --- a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureBuilder.kt +++ b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureBuilder.kt @@ -9,6 +9,9 @@ import org.axonframework.messaging.MessageHandlerInterceptor import org.axonframework.messaging.annotation.HandlerDefinition import org.axonframework.messaging.annotation.HandlerEnhancerDefinition import org.axonframework.messaging.annotation.ParameterResolverFactory +import org.axonframework.messaging.correlation.CorrelationDataProvider +import org.axonframework.messaging.correlation.MessageOriginProvider +import org.axonframework.messaging.interceptors.CorrelationDataInterceptor import org.axonframework.modelling.command.CommandTargetResolver import org.axonframework.modelling.command.Repository import org.axonframework.modelling.command.RepositoryProvider @@ -36,6 +39,7 @@ class AggregateTestFixtureBuilder(private val aggregateType: Class) { private lateinit var repository: Repository private lateinit var repositoryProvider: RepositoryProvider private lateinit var subtypes: Array> + private val correlationDataProviders = mutableListOf(MessageOriginProvider()) fun registerAggregateFactory(aggregateFactory: AggregateFactory) = apply { this.aggregateFactory = aggregateFactory } fun registerAnnotatedCommandHandler(annotatedCommandHandler: Any) = apply { this.annotatedCommandHandler.add(annotatedCommandHandler) } @@ -85,6 +89,10 @@ class AggregateTestFixtureBuilder(private val aggregateType: Class) { fun withSubtypes(vararg subtypes: Class) = withSubtypes(subtypes.asList()) fun withSubtypes(subtypes: Collection>) = apply { this.subtypes = subtypes.toTypedArray() } + fun registerCorrelationDataProvider(correlationDataProvider: CorrelationDataProvider) = apply { + this.correlationDataProviders.add(correlationDataProvider) + } + fun build(): AggregateTestFixture { val fixture = AggregateTestFixture(aggregateType) @@ -112,6 +120,11 @@ class AggregateTestFixtureBuilder(private val aggregateType: Class) { if (this::subtypes.isInitialized) fixture.withSubtypes(*this.subtypes) reportIllegalStateChange?.let { fixture.setReportIllegalStateChange(it) } + if (correlationDataProviders.isNotEmpty()) { + fixture.commandBus.registerHandlerInterceptor( + CorrelationDataInterceptor(*(correlationDataProviders.toTypedArray())) + ) + } return fixture } } diff --git a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureContext.kt b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureContext.kt index 65df864..ff74910 100644 --- a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureContext.kt +++ b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureContext.kt @@ -1,5 +1,7 @@ package io.holixon.axon.testing.jgiven.aggregate +import io.holixon.axon.testing.jgiven.aggregate.AggregateTestFixtureReflection.aggregateIdentifier +import io.holixon.axon.testing.jgiven.aggregate.AggregateTestFixtureReflection.buildResultValidator import org.axonframework.test.aggregate.AggregateTestFixture import org.axonframework.test.aggregate.ResultValidator import org.axonframework.test.aggregate.TestExecutor @@ -13,7 +15,7 @@ internal class AggregateTestFixtureContext( constructor(fixture: AggregateTestFixture) : this( fixture, fixture.givenNoPriorActivity(), - AggregateTestFixtureExt.buildResultValidator(fixture) + buildResultValidator(fixture) ) fun checkInitialized() { @@ -25,4 +27,6 @@ internal class AggregateTestFixtureContext( fun isInitialized() = fixture != null && testExecutor != null && resultValidator != null var isFirstGiven = true + + val aggregateIdentifier: String? get() = fixture?.aggregateIdentifier } diff --git a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureExt.kt b/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureExt.kt deleted file mode 100644 index a630945..0000000 --- a/extension/jgiven/core/src/main/kotlin/aggregate/AggregateTestFixtureExt.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.holixon.axon.testing.jgiven.aggregate - -import org.axonframework.test.aggregate.AggregateTestFixture -import org.axonframework.test.aggregate.ResultValidator -import java.lang.reflect.Method - -object AggregateTestFixtureExt { - - private val method_buildResultValidator: Method = AggregateTestFixture::class.java.getDeclaredMethod("buildResultValidator").apply { - isAccessible = true - } - - @Suppress("UNCHECKED_CAST") - fun buildResultValidator(fixture:AggregateTestFixture) = method_buildResultValidator.invoke(fixture) as ResultValidator -} diff --git a/extension/jgiven/core/src/main/kotlin/aggregate/_reflection.kt b/extension/jgiven/core/src/main/kotlin/aggregate/_reflection.kt new file mode 100644 index 0000000..a66c717 --- /dev/null +++ b/extension/jgiven/core/src/main/kotlin/aggregate/_reflection.kt @@ -0,0 +1,19 @@ +package io.holixon.axon.testing.jgiven.aggregate + +import io.holixon.axon.testing.jgiven.AxonJGiven.getDeclaredAccessibleField +import io.holixon.axon.testing.jgiven.AxonJGiven.getDeclaredAccessibleMethod +import org.axonframework.test.aggregate.AggregateTestFixture +import org.axonframework.test.aggregate.ResultValidator +import java.lang.reflect.Field +import java.lang.reflect.Method + +internal object AggregateTestFixtureReflection { + + private val method_buildResultValidator: Method = AggregateTestFixture::class.java.getDeclaredAccessibleMethod("buildResultValidator") + private val field_aggregateIdentifier: Field = AggregateTestFixture::class.java.getDeclaredAccessibleField("aggregateIdentifier") + + @Suppress("UNCHECKED_CAST") + fun buildResultValidator(fixture: AggregateTestFixture): ResultValidator = method_buildResultValidator.invoke(fixture) as ResultValidator + + val AggregateTestFixture.aggregateIdentifier: String? get() = field_aggregateIdentifier.get(this) as String? +} diff --git a/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt b/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt index f304112..8f1fe70 100644 --- a/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt +++ b/extension/jgiven/core/src/main/kotlin/saga/_reflection.kt @@ -1,5 +1,6 @@ package io.holixon.axon.testing.jgiven.saga +import io.holixon.axon.testing.jgiven.AxonJGiven.getDeclaredAccessibleField 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 @@ -15,11 +16,6 @@ import java.lang.reflect.Field 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") diff --git a/extension/jgiven/core/src/test/kotlin/AxonJGivenTestFixtures.kt b/extension/jgiven/core/src/test/kotlin/AxonJGivenTestFixtures.kt index 3bec257..9c6b2c9 100644 --- a/extension/jgiven/core/src/test/kotlin/AxonJGivenTestFixtures.kt +++ b/extension/jgiven/core/src/test/kotlin/AxonJGivenTestFixtures.kt @@ -26,19 +26,14 @@ object AxonJGivenTestFixtures { class DummyAggregate() { - companion object { - - @CommandHandler - @JvmStatic - fun create(cmd: CreateDummyAggregate) = DummyAggregate().apply { - AggregateLifecycle.apply(DummyAggregateCreated(cmd.id)) - } - - } - @AggregateIdentifier lateinit var id: String + @CommandHandler + constructor(cmd: CreateDummyAggregate) : this() { + AggregateLifecycle.apply(DummyAggregateCreated(cmd.id)) + } + @EventSourcingHandler fun on(evt: DummyAggregateCreated) { this.id = evt.id diff --git a/extension/jgiven/core/src/test/kotlin/aggregate/AggregateTestFixtureBuilderTest.kt b/extension/jgiven/core/src/test/kotlin/aggregate/AggregateTestFixtureBuilderTest.kt new file mode 100644 index 0000000..7a74217 --- /dev/null +++ b/extension/jgiven/core/src/test/kotlin/aggregate/AggregateTestFixtureBuilderTest.kt @@ -0,0 +1,74 @@ +package io.holixon.axon.testing.jgiven.aggregate + +import com.tngtech.jgiven.annotation.ProvidedScenarioState +import io.holixon.axon.testing.jgiven.AxonJGiven +import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.AggregateFixtureScenarioTest +import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.CreateDummyAggregate +import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.DummyAggregate +import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.DummyAggregateCreated +import io.toolisticon.testing.jgiven.GIVEN +import io.toolisticon.testing.jgiven.THEN +import io.toolisticon.testing.jgiven.WHEN +import org.axonframework.commandhandling.CommandMessage +import org.axonframework.messaging.MessageDispatchInterceptor +import org.axonframework.messaging.correlation.SimpleCorrelationDataProvider +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.util.function.BiFunction + +internal class AggregateTestFixtureBuilderTest { + + @Nested + inner class RegisterCorrelationDataProvider : AggregateFixtureScenarioTest() { + + private fun addFooMetaToEveryCommand(value: String) = MessageDispatchInterceptor> { + BiFunction { _, cmd -> cmd.andMetaData(mapOf("foo" to value)) } + } + + @ProvidedScenarioState + val fixture = AxonJGiven.aggregateTestFixtureBuilder() + .registerCommandDispatchInterceptor(addFooMetaToEveryCommand("bar")) + .registerCorrelationDataProvider(SimpleCorrelationDataProvider("foo")) + .build() + + @Test + fun `metaData foo=bar is propagated to event`() { + GIVEN.noPriorActivity() + + WHEN.command(CreateDummyAggregate("1")) + + THEN + .expectEventWithMetaData(DummyAggregateCreated("1"), "foo" to "bar") + } + } + + + @Nested + inner class EmptyFixture : AggregateFixtureScenarioTest() { + + @ProvidedScenarioState + val fixture = AxonJGiven.aggregateTestFixtureBuilder().build() + + @Test + fun `create aggregate`() { + GIVEN + .noPriorActivity() + + WHEN + .command(CreateDummyAggregate("1")) + + THEN + .expectEvent(DummyAggregateCreated("1")) + } + + @Test + fun `just no events`() { + GIVEN + .noPriorActivity() + + THEN + .expectNoEvents() + } + } + +} diff --git a/extension/jgiven/core/src/test/kotlin/aggregate/DummyAggregateTest.kt b/extension/jgiven/core/src/test/kotlin/aggregate/DummyAggregateTest.kt deleted file mode 100644 index 414c9af..0000000 --- a/extension/jgiven/core/src/test/kotlin/aggregate/DummyAggregateTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.holixon.axon.testing.jgiven.aggregate - -import com.tngtech.jgiven.annotation.ProvidedScenarioState -import io.holixon.axon.testing.jgiven.AxonJGiven.aggregateTestFixtureBuilder -import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures -import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.CreateDummyAggregate -import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.DummyAggregate -import io.holixon.axon.testing.jgiven.AxonJGivenTestFixtures.DummyAggregateCreated -import io.toolisticon.testing.jgiven.GIVEN -import io.toolisticon.testing.jgiven.THEN -import io.toolisticon.testing.jgiven.WHEN -import org.junit.jupiter.api.Test - -class DummyAggregateTest : AxonJGivenTestFixtures.AggregateFixtureScenarioTest() { - - @ProvidedScenarioState - val fixture = aggregateTestFixtureBuilder() - .build() - - @Test - fun `create aggregate`() { - GIVEN - .noPriorActivity() - - WHEN - .command(CreateDummyAggregate("1")) - - THEN - .expectEvent(DummyAggregateCreated("1")) - } - - @Test - fun `just no events`() { - GIVEN - .noPriorActivity() - - THEN - .expectNoEvents() - } -}