Skip to content

Commit

Permalink
feat(364): provide CorrelationDataProviders and test for payload+meta (
Browse files Browse the repository at this point in the history
…#365)

fixes #364
  • Loading branch information
jangalinski authored Nov 21, 2023
1 parent f4da660 commit b0a307f
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 73 deletions.
6 changes: 6 additions & 0 deletions extension/jgiven/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
<version>1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down
24 changes: 24 additions & 0 deletions extension/jgiven/core/src/main/kotlin/AxonJGiven.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,6 +31,26 @@ object AxonJGiven {
inline fun <reified T : Any> aggregateTestFixtureBuilder() = AggregateTestFixtureBuilder(T::class.java)
inline fun <reified T : Any> 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<String, Any>): Predicate<List<DomainEventMessage<*>>> = 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 }
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +31,9 @@ import kotlin.reflect.KClass
class AggregateFixtureThen<T> : Stage<AggregateFixtureThen<T>>() {

@ProvidedScenarioState
private var context: AggregateTestFixtureContext<T> = AggregateTestFixtureContext()
private var context: AggregateTestFixtureContext<T> = AggregateTestFixtureContext()

private val reporter = Reporter()

/**
* Expect event.
Expand All @@ -34,6 +42,21 @@ class AggregateFixtureThen<T> : Stage<AggregateFixtureThen<T>>() {
@As("expect event:")
fun expectEvent(event: Any) = this.expectEvents(event)

@As("expect event:\$ with metaData:\$")
fun expectEventWithMetaData(event: Any, vararg entries: Pair<String, Any>) = 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.
Expand Down Expand Up @@ -172,6 +195,7 @@ class AggregateFixtureThen<T> : Stage<AggregateFixtureThen<T>>() {
@As("expect result message")
fun expectResultMessage(message: CommandResultMessage<*>) = execute {
expectResultMessage(message)

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +39,7 @@ class AggregateTestFixtureBuilder<T>(private val aggregateType: Class<T>) {
private lateinit var repository: Repository<T>
private lateinit var repositoryProvider: RepositoryProvider
private lateinit var subtypes: Array<Class<out T>>
private val correlationDataProviders = mutableListOf<CorrelationDataProvider>(MessageOriginProvider())

fun registerAggregateFactory(aggregateFactory: AggregateFactory<T>) = apply { this.aggregateFactory = aggregateFactory }
fun registerAnnotatedCommandHandler(annotatedCommandHandler: Any) = apply { this.annotatedCommandHandler.add(annotatedCommandHandler) }
Expand Down Expand Up @@ -85,6 +89,10 @@ class AggregateTestFixtureBuilder<T>(private val aggregateType: Class<T>) {
fun withSubtypes(vararg subtypes: Class<out T>) = withSubtypes(subtypes.asList())
fun withSubtypes(subtypes: Collection<Class<out T>>) = apply { this.subtypes = subtypes.toTypedArray() }

fun registerCorrelationDataProvider(correlationDataProvider: CorrelationDataProvider) = apply {
this.correlationDataProviders.add(correlationDataProvider)
}

fun build(): AggregateTestFixture<T> {
val fixture = AggregateTestFixture<T>(aggregateType)

Expand Down Expand Up @@ -112,6 +120,11 @@ class AggregateTestFixtureBuilder<T>(private val aggregateType: Class<T>) {
if (this::subtypes.isInitialized) fixture.withSubtypes(*this.subtypes)
reportIllegalStateChange?.let { fixture.setReportIllegalStateChange(it) }

if (correlationDataProviders.isNotEmpty()) {
fixture.commandBus.registerHandlerInterceptor(
CorrelationDataInterceptor(*(correlationDataProviders.toTypedArray()))
)
}
return fixture
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,7 +15,7 @@ internal class AggregateTestFixtureContext<T>(
constructor(fixture: AggregateTestFixture<T>) : this(
fixture,
fixture.givenNoPriorActivity(),
AggregateTestFixtureExt.buildResultValidator(fixture)
buildResultValidator(fixture)
)

fun checkInitialized() {
Expand All @@ -25,4 +27,6 @@ internal class AggregateTestFixtureContext<T>(
fun isInitialized() = fixture != null && testExecutor != null && resultValidator != null

var isFirstGiven = true

val aggregateIdentifier: String? get() = fixture?.aggregateIdentifier
}

This file was deleted.

19 changes: 19 additions & 0 deletions extension/jgiven/core/src/main/kotlin/aggregate/_reflection.kt
Original file line number Diff line number Diff line change
@@ -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 <T> buildResultValidator(fixture: AggregateTestFixture<T>): ResultValidator<T> = method_buildResultValidator.invoke(fixture) as ResultValidator<T>

val <T> AggregateTestFixture<T>.aggregateIdentifier: String? get() = field_aggregateIdentifier.get(this) as String?
}
6 changes: 1 addition & 5 deletions extension/jgiven/core/src/main/kotlin/saga/_reflection.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand Down
15 changes: 5 additions & 10 deletions extension/jgiven/core/src/test/kotlin/AxonJGivenTestFixtures.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DummyAggregate>() {

private fun addFooMetaToEveryCommand(value: String) = MessageDispatchInterceptor<CommandMessage<*>> {
BiFunction { _, cmd -> cmd.andMetaData(mapOf("foo" to value)) }
}

@ProvidedScenarioState
val fixture = AxonJGiven.aggregateTestFixtureBuilder<DummyAggregate>()
.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<DummyAggregate>() {

@ProvidedScenarioState
val fixture = AxonJGiven.aggregateTestFixtureBuilder<DummyAggregate>().build()

@Test
fun `create aggregate`() {
GIVEN
.noPriorActivity()

WHEN
.command(CreateDummyAggregate("1"))

THEN
.expectEvent(DummyAggregateCreated("1"))
}

@Test
fun `just no events`() {
GIVEN
.noPriorActivity()

THEN
.expectNoEvents()
}
}

}

This file was deleted.

0 comments on commit b0a307f

Please sign in to comment.