dk.cloudcreate.essentials.components.kotlin.eventsourcing.test.GivenWhenThenScenario.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-eventsourcing Show documentation
Show all versions of kotlin-eventsourcing Show documentation
This library focuses purely on providing event sourcing support for Kotlin
The newest version!
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dk.cloudcreate.essentials.components.kotlin.eventsourcing.test
import dk.cloudcreate.essentials.components.kotlin.eventsourcing.Decider
import java.util.function.Consumer
import kotlin.reflect.KClass
/**
* Single use Given/When/Then scenario runner and asserter
*
* Assertions exceptions thrown from [then_], [thenExpectNoEvent], [thenFailsWithException], [thenFailsWithExceptionType]
* are all subclasses of [AssertionException]
*
* Example:
* ```
* val scenario = GivenWhenThenScenario(ShipOrderDecider())
*
* var orderId = OrderId.random()
* scenario
* .given(
* OrderCreated(orderId),
* OrderAccepted(orderId)
* )
* .when_(ShipOrder(orderId))
* .then_(OrderShipped(orderId))
* ```
* And [Decider] example
* ```
* class ShipOrderDecider : Decider {
* override fun handle(cmd: ShipOrder, events: List): OrderEvent? {
* if (events.isEmpty()) {
* throw RuntimeException("Cannot accept an order that hasn't been created")
* }
* if (events.any { it is OrderShipped}) {
* // Already shipped - idempotent handling
* return null
* }
* if (!events.any { it is OrderAccepted }) {
* throw RuntimeException("Cannot ship an order that hasn't been accepted")
* }
* return OrderShipped(cmd.id)
* }
* }
* ```
*/
class GivenWhenThenScenario(val decider: Decider) {
var _givenEvents: List = listOf()
var _whenCommand: CMD? = null
var _actualEvent: EVENT? = null
lateinit var _expectException: Exception
lateinit var _expectedExceptionType: KClass
/**
* Set up the test scenario by providing past events that will be provided to
* the [Decider.handle] method as an event stream.
*
* This step is also known as the **Arrange** step in Arrange, Act, Assert
*
* In case this method isn't called, then the default value for [_givenEvents] is an empty list
* @param events the past events related to the aggregate instance (CAN be empty)
* @return this [GivenWhenThenScenario] instance
*/
fun given(vararg events: EVENT): GivenWhenThenScenario {
_givenEvents = events.toList()
return this
}
/**
* Define the command that will be supplied to the [Decider.handle] as the command when one of the **then** methods
* are called
*
* This step is also known as the **Act** step in Arrange, Act, Assert
* @param cmd the command
* @return this [GivenWhenThenScenario] instance
*/
fun when_(cmd: CMD): GivenWhenThenScenario {
_whenCommand = cmd
return this
}
/**
* Define the expected event outcome when [GivenWhenThenScenario] is calling the [Decider.handle] with the command
* provided in [when_] and past events provided in [given]
*
* This step is also known as the **Assert** step in Arrange, Act, Assert
* @param expectedEvent The event we expect to be returned from the [Decider.handle] method as a result of
* handling the [_whenCommand] using the [_givenEvents] past events
* @return this [GivenWhenThenScenario] instance
* @throws DidNotExpectAnEventException
* @throws ExpectedAnEventButDidGetAnyEventException
* @throws ActualAndExpectedEventsAreNotEqualExcepted
* @throws FailedWithUnexpectException
*/
fun then_(expectedEvent: EVENT?): GivenWhenThenScenario {
if (_whenCommand == null) throw NoCommandProvidedException()
try {
_actualEvent = decider.handle(_whenCommand!!, _givenEvents)
if (_actualEvent != expectedEvent) {
if (expectedEvent == null) {
throw DidNotExpectAnEventException(_actualEvent!!)
}
if (_actualEvent == null) {
throw ExpectedAnEventButDidGetAnyEventException(expectedEvent)
}
throw ActualAndExpectedEventsAreNotEqualExcepted(expectedEvent, _actualEvent!!)
}
return this
} catch (e: Exception) {
throw FailedWithUnexpectException(e)
}
}
/**
* Define the expected event outcome when [GivenWhenThenScenario] is calling the [Decider.handle] with the command
* provided in [when_] and past events provided in [given]
*
* This step is also known as the **Assert** step in Arrange, Act, Assert
* @param actualEventAsserter A [Consumer] that will receive the **actual** event that resulted from handling
* handling the [_whenCommand] using the [_givenEvents] past events. This [Consumer] is expected to manually assert
* the content of the actual event
* @return this [GivenWhenThenScenario] instance
*/
fun thenAssert(actualEventAsserter: Consumer): GivenWhenThenScenario {
if (_whenCommand == null) throw NoCommandProvidedException()
try {
_actualEvent = decider.handle(_whenCommand!!, _givenEvents)
actualEventAsserter.accept(_actualEvent)
return this
} catch (e: Exception) {
throw FailedWithUnexpectException(e)
}
}
/**
* Define that we don't expect any events outcome when [GivenWhenThenScenario] is calling the [Decider.handle] with the command
* provided in [when_] and past events provided in [given]
*
* This step is also known as the **Assert** step in Arrange, Act, Assert
* @param expectedEvent The event we expect to be returned from the [Decider.handle] method as a result of
* handling the [_whenCommand] using the [_givenEvents] past events
* @return this [GivenWhenThenScenario] instance
* @throws DidNotExpectAnEventException
* @throws ExpectedAnEventButDidGetAnyEventException
* @throws ActualAndExpectedEventsAreNotEqualExcepted
* @throws FailedWithUnexpectException
*/
fun thenExpectNoEvent(): GivenWhenThenScenario {
then_(null)
return this
}
/**
* Define that we expect the scenario to fail with an [expectedException] of a given [Exception]
* instance when the [GivenWhenThenScenario] is calling the [Decider.handle] with the command
* provided in [when_] and past events provided in [given]
*
* This step is also known as the **Assert** step in Arrange, Act, Assert
* @param expectedException The exception that we expect the [Decider.handle] to throw
* when handling the [_whenCommand] using the [_givenEvents] past events
* @return this [GivenWhenThenScenario] instance
* @throws ExpectToFailWithAnExceptionButNoneWasThrown
* @throws ActualExceptionIsNotEqualToExpectedException
*/
fun thenFailsWithException(expectedException: Exception): GivenWhenThenScenario {
this._expectException = expectedException
try {
decider.handle(_whenCommand!!, _givenEvents)
throw ExpectToFailWithAnExceptionButNoneWasThrown(expectedException)
} catch (actualException: Exception) {
if (actualException::class != expectedException::class) {
throw ActualExceptionIsNotEqualToExpectedException(expectedException, actualException)
}
// TODO: Allow the call to specify how exception instances should be compared
if (actualException.message != expectedException.message) {
throw ActualExceptionIsNotEqualToExpectedException(expectedException, actualException)
}
return this
}
}
/**
* Define that we expect the scenario to fail with an [expectedExceptionType] of a specific [Exception] type
* when the [GivenWhenThenScenario] is calling the [Decider.handle] with the command
* provided in [when_] and past events provided in [given]
*
* This step is also known as the **Assert** step in Arrange, Act, Assert
* @param expectedException The exception that we expect the [Decider.handle] to throw
* when handling the [_whenCommand] using the [_givenEvents] past events
* @return this [GivenWhenThenScenario] instance
* @throws ExpectToFailWithAnExceptionButNoneWasThrown
* @throws ActualExceptionIsNotEqualToExpectedException
*/
fun thenFailsWithExceptionType(expectedExceptionType: KClass): GivenWhenThenScenario {
this._expectedExceptionType = expectedExceptionType
try {
decider.handle(_whenCommand!!, _givenEvents)
throw ExpectToFailWithAnExceptionTypeButNoneWasThrown(expectedExceptionType)
} catch (actualException: Exception) {
val actualExceptionType = actualException::class
if (actualExceptionType != expectedExceptionType) {
throw ActualExceptionTypeIsNotEqualToExpectedException(expectedExceptionType, actualException)
}
return this
}
}
}
abstract class AssertionException(msg: String?, e: Exception?) : RuntimeException(msg, e) {
constructor(exception: Exception) : this(null, exception)
constructor(msg: String) : this(msg, null)
constructor() : this(null, null)
}
class NoCommandProvidedException : AssertionException()
class FailedWithUnexpectException(val unexpectedException: Exception) : AssertionException(unexpectedException)
class ExpectToFailWithAnExceptionButNoneWasThrown(val expectedException: Exception) :
AssertionException(expectedException)
class ExpectToFailWithAnExceptionTypeButNoneWasThrown(val expectedExceptionType: KClass) :
AssertionException("No exception thrown. Expected exception of type $expectedExceptionType")
class ActualExceptionTypeIsNotEqualToExpectedException(
val expectedExceptionType: KClass,
val actualException: Exception
) : AssertionException("Expected exception of type $expectedExceptionType, but got", actualException)
class ActualExceptionIsNotEqualToExpectedException(val expectedException: Exception, val actualException: Exception) :
AssertionException("Actual exception ${actualException::class.simpleName} is not equal to expected exception ${expectedException::class.simpleName} ")
class DidNotExpectAnEventException(val actualEvent: Any) : AssertionException("Did not expect event: $actualEvent")
class ExpectedAnEventButDidGetAnyEventException(val expectedEvent: Any) :
AssertionException("Did not get an event. Expected event: $expectedEvent")
class ActualAndExpectedEventsAreNotEqualExcepted(val expectedEvent: Any, val actualEvent: Any) :
AssertionException("Got actual event: $actualEvent, but expected event: $expectedEvent")
© 2015 - 2025 Weber Informatics LLC | Privacy Policy