test: rework GameSimulationTest for greater robustness
This commit is contained in:
@@ -31,6 +31,9 @@ class PlayerNotificationListener(
|
|||||||
) {
|
) {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward projection from [bus][GameProjectionBus] to the player [notification][outgoingNotification]
|
||||||
|
*/
|
||||||
fun startListening(
|
fun startListening(
|
||||||
currentPlayer: Player,
|
currentPlayer: Player,
|
||||||
gameId: GameId,
|
gameId: GameId,
|
||||||
@@ -39,7 +42,7 @@ class PlayerNotificationListener(
|
|||||||
return projectionBus.subscribe { currentState ->
|
return projectionBus.subscribe { currentState ->
|
||||||
if (currentState !is GameState) return@subscribe
|
if (currentState !is GameState) return@subscribe
|
||||||
if (currentState.aggregateId != gameId) return@subscribe
|
if (currentState.aggregateId != gameId) return@subscribe
|
||||||
withLoggingContext("projection" to currentState.toString()) {
|
withLoggingContext("currentPlayer" to currentPlayer.toString(), "projection" to currentState.toString()) {
|
||||||
fun Notification.send() {
|
fun Notification.send() {
|
||||||
withLoggingContext("notification" to this.toString()) {
|
withLoggingContext("notification" to this.toString()) {
|
||||||
if (currentState.players.contains(currentPlayer)) {
|
if (currentState.players.contains(currentPlayer)) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eventDemo.adapter.interfaceLayer.query
|
package eventDemo.adapter.interfaceLayer.query
|
||||||
|
|
||||||
import eventDemo.Tag
|
import eventDemo.Tag
|
||||||
|
import eventDemo.adapter.infrastructureLayer.event.projection.GameStateRepositoryInMemory
|
||||||
import eventDemo.business.command.GameCommandHandler
|
import eventDemo.business.command.GameCommandHandler
|
||||||
import eventDemo.business.command.command.GameCommand
|
import eventDemo.business.command.command.GameCommand
|
||||||
import eventDemo.business.command.command.IWantToJoinTheGameCommand
|
import eventDemo.business.command.command.IWantToJoinTheGameCommand
|
||||||
@@ -12,7 +13,6 @@ import eventDemo.business.entity.Player
|
|||||||
import eventDemo.business.event.GameEventStore
|
import eventDemo.business.event.GameEventStore
|
||||||
import eventDemo.business.event.event.disableShuffleDeck
|
import eventDemo.business.event.event.disableShuffleDeck
|
||||||
import eventDemo.business.event.projection.gameState.GameState
|
import eventDemo.business.event.projection.gameState.GameState
|
||||||
import eventDemo.business.event.projection.gameState.apply
|
|
||||||
import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener
|
import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener
|
||||||
import eventDemo.business.notification.CommandSuccessNotification
|
import eventDemo.business.notification.CommandSuccessNotification
|
||||||
import eventDemo.business.notification.ItsTheTurnOfNotification
|
import eventDemo.business.notification.ItsTheTurnOfNotification
|
||||||
@@ -22,11 +22,10 @@ import eventDemo.business.notification.PlayerAsPlayACardNotification
|
|||||||
import eventDemo.business.notification.PlayerWasReadyNotification
|
import eventDemo.business.notification.PlayerWasReadyNotification
|
||||||
import eventDemo.business.notification.TheGameWasStartedNotification
|
import eventDemo.business.notification.TheGameWasStartedNotification
|
||||||
import eventDemo.business.notification.WelcomeToTheGameNotification
|
import eventDemo.business.notification.WelcomeToTheGameNotification
|
||||||
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
|
|
||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
|
import io.kotest.assertions.nondeterministic.eventually
|
||||||
import io.kotest.assertions.nondeterministic.until
|
import io.kotest.assertions.nondeterministic.until
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -36,7 +35,6 @@ import kotlinx.coroutines.channels.trySendBlocking
|
|||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlin.test.assertIs
|
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -63,141 +61,124 @@ class GameSimulationTest :
|
|||||||
var player1HasJoin = false
|
var player1HasJoin = false
|
||||||
|
|
||||||
testKoinApplicationWithConfig {
|
testKoinApplicationWithConfig {
|
||||||
|
val commandHandler by inject<GameCommandHandler>()
|
||||||
|
val eventStore by inject<GameEventStore>()
|
||||||
|
val playerNotificationListener by inject<PlayerNotificationListener>()
|
||||||
|
|
||||||
|
// Run command handler
|
||||||
|
// In the normal process, these handlers is invoque players connect to the websocket
|
||||||
|
run {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player1, gameId, channelCommand1, channelNotification1)
|
||||||
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player2, gameId, channelCommand2, channelNotification2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume etch notification of players, and put theses in list.
|
||||||
|
// is used later to control when other players can be executing the next action
|
||||||
|
val player1Notifications = mutableListOf<Notification>()
|
||||||
|
val player2Notifications = mutableListOf<Notification>()
|
||||||
|
run {
|
||||||
|
GlobalScope.launch {
|
||||||
|
for (notification in channelNotification1) {
|
||||||
|
player1Notifications.add(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
for (notification in channelNotification2) {
|
||||||
|
player2Notifications.add(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player 1 actions
|
||||||
val player1Job =
|
val player1Job =
|
||||||
launch {
|
launch {
|
||||||
|
playerNotificationListener.startListening(player1, gameId) {
|
||||||
|
channelNotification1.trySendBlocking(it)
|
||||||
|
}
|
||||||
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
|
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
|
||||||
channelCommand1.send(sendCommand)
|
channelCommand1.send(sendCommand)
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player1HasJoin = true
|
player1HasJoin = true
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1) }
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> { player == player2 }
|
||||||
}
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
|
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
|
||||||
channelCommand1.send(sendCommand)
|
channelCommand1.send(sendCommand)
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<PlayerWasReadyNotification> { player == player2 }
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
val player1Hand = player1Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
|
||||||
}
|
|
||||||
val player1Hand =
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
|
||||||
playedCard1 = player1Hand.first()
|
playedCard1 = player1Hand.first()
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
|
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
|
||||||
channelCommand1.send(sendCommand)
|
channelCommand1.send(sendCommand)
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
player1Notifications.waitNotification<PlayerAsPlayACardNotification> {
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
player == player2 && card == playedCard2
|
||||||
player shouldBeEqual player2
|
|
||||||
card shouldBeEqual assertNotNull(playedCard2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The player 2 actions
|
||||||
val player2Job =
|
val player2Job =
|
||||||
launch {
|
launch {
|
||||||
|
// wait the player 1 has join the game
|
||||||
until(1.seconds) { player1HasJoin }
|
until(1.seconds) { player1HasJoin }
|
||||||
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
|
|
||||||
channelCommand2.send(sendCommand)
|
playerNotificationListener.startListening(player2, gameId) {
|
||||||
channelNotification2.receive().let {
|
channelNotification2.trySendBlocking(it)
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channelNotification2.receive().let {
|
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
channelCommand2.send(sendCommand)
|
||||||
}
|
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player2Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1, player2) }
|
||||||
|
player2Notifications.waitNotification<PlayerWasReadyNotification> { player == player1 }
|
||||||
|
|
||||||
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
|
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
|
||||||
channelCommand2.send(sendCommand)
|
channelCommand2.send(sendCommand)
|
||||||
channelNotification2.receive().let {
|
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val player2Hand =
|
val player2Hand =
|
||||||
channelNotification2.receive().let {
|
player2Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
|
||||||
channelNotification2.receive().let {
|
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
player == player1 && card == playedCard1
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
card shouldBeEqual assertNotNull(playedCard1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
playedCard2 = player2Hand.first()
|
playedCard2 = player2Hand.first()
|
||||||
|
|
||||||
channelNotification2.receive().let {
|
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
|
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
|
||||||
channelCommand2.send(sendCommand)
|
channelCommand2.send(sendCommand)
|
||||||
channelNotification2.receive().let {
|
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val commandHandler by inject<GameCommandHandler>()
|
// Wait the end of the game
|
||||||
val eventStore by inject<GameEventStore>()
|
|
||||||
val playerNotificationListener by inject<PlayerNotificationListener>()
|
|
||||||
playerNotificationListener.startListening(player1, gameId) { channelNotification1.trySendBlocking(it) }
|
|
||||||
playerNotificationListener.startListening(player2, gameId) { channelNotification2.trySendBlocking(it) }
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
commandHandler.handle(player1, gameId, channelCommand1, channelNotification1)
|
|
||||||
}
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
commandHandler.handle(player2, gameId, channelCommand2, channelNotification2)
|
|
||||||
}
|
|
||||||
|
|
||||||
joinAll(player1Job, player2Job)
|
joinAll(player1Job, player2Job)
|
||||||
|
|
||||||
val state =
|
// Build the last state from the event store
|
||||||
ProjectionSnapshotRepositoryInMemory(
|
val state = GameStateRepositoryInMemory(eventStore = eventStore).getLast(gameId)
|
||||||
eventStore = eventStore,
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
|
||||||
applyToProjection = GameState::apply,
|
|
||||||
).getLast(gameId)
|
|
||||||
|
|
||||||
|
// Check if the state is correct
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
assertTrue(state.isStarted)
|
assertTrue(state.isStarted)
|
||||||
state.players shouldBeEqual setOf(player1, player2)
|
state.players shouldBeEqual setOf(player1, player2)
|
||||||
@@ -209,3 +190,8 @@ class GameSimulationTest :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T =
|
||||||
|
eventually(1.seconds) {
|
||||||
|
filterIsInstance<T>().first { block(it) }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user