test: rework GameSimulationTest for greater robustness

This commit is contained in:
2025-04-12 03:25:28 +02:00
parent af1e5ee22a
commit f85f5986b4
2 changed files with 84 additions and 95 deletions

View File

@@ -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)) {

View File

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