diff --git a/build.gradle.kts b/build.gradle.kts index 694249c..5ca3e1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("io.kotest:kotest-assertions-core:5.9.1") testImplementation("org.junit.jupiter:junit-jupiter-params:5.1.0") + testImplementation("io.mockk:mockk:1.13.12") } tasks.test { diff --git a/src/main/kotlin/main.kt b/src/main/kotlin/main.kt index 2dfb5cf..55acc01 100644 --- a/src/main/kotlin/main.kt +++ b/src/main/kotlin/main.kt @@ -1,30 +1,51 @@ -fun main() { - val game = Game() - game() -} +fun main() = RepeatGames().start() class Game( - private val rnd: (List) -> String = { it.random() } + private val rnd: (List) -> String = { it.random() }, + private val input: UserInput = UserInput(), + private val output: UserOutput = UserOutput() ) { operator fun invoke() { val choices = listOf("rock", "paper", "scissors") val compChoice = rnd(choices) - println("Enter choice:") - val userIn = readln() - if (userIn !in choices) { - println("Invalid input: $userIn") + output.display("Enter choice:") + val userIn = input.line() + if(userIn !in choices) { + output.display("Invalid input: $userIn") return } - println("Computer choice: $compChoice") - if (compChoice == userIn) { - println("draw") + + output.display("Computer choice: $compChoice") + if(compChoice == userIn) { + output.display("draw") return } - when ("$compChoice $userIn") { - "rock paper" -> println("win") - "paper scissors" -> println("win") - "scissors rock" -> println("win") - else -> println("lose") + when("$compChoice $userIn") { + "rock paper" -> output.display("win") + "paper scissors" -> output.display("win") + "scissors rock" -> output.display("win") + else -> output.display("lose") } } } + +class RepeatGames( + private val output: UserOutput = UserOutput(), + private val input: UserInput = UserInput(), + private val generator: () -> Game = { Game() } +) { + fun start() { + do { + generator()() + output.display("Play another game (Enter 'n' or 'no' to exit)?") + } while(input.line().orEmpty().lowercase() !in arrayOf("n", "no")) + } +} + +class UserInput { + fun line() : String? = readlnOrNull() +} + +class UserOutput { + fun display(line: String) = println(line) +} diff --git a/src/test/kotlin/MainKtTest.kt b/src/test/kotlin/MainKtTest.kt index 64bdbe0..60fb88b 100644 --- a/src/test/kotlin/MainKtTest.kt +++ b/src/test/kotlin/MainKtTest.kt @@ -1,100 +1,127 @@ import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldEndWith -import org.junit.jupiter.api.AfterEach +import io.mockk.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.PrintStream class MainKtTest { - private val oIn = System.`in` - private val oOut = System.out - - private lateinit var console: ByteArrayOutputStream + private lateinit var input: UserInput + private lateinit var output: UserOutput @BeforeEach fun setUp() { - console = ByteArrayOutputStream() - System.setOut(PrintStream(console)) - } + input = mockk() + every { input.line() } returns "" - @AfterEach - fun tearDown() { - System.setIn(oIn) - System.setOut(oOut) + output = mockk() + every { output.display(any()) } just runs } @Test - fun `when user enters unrecognised input, then exit with invalid input warning`() { - val user = "Neither a rock nor paper nor scissors" - user.enterAsUserInput() + fun `when user enters unrecognised input, then exit with invalid input warning`() = with("Invalid input") { + enterUserInput() runGame() - console.asString() shouldEndWith "Invalid input: $user" - + verifyOutput { + it shouldEndWith "Invalid input: $this" + } } + @Test fun `when computer player randomly selects rock, then print computer player choice`() { - "rock".enterAsUserInput() + "rock".enterUserInput() val computer = "scissors" runGameWithComputerChoice(computer) - console.asString() shouldContain "Computer choice: $computer" + verifyOutput { + it shouldContain "Computer choice: $computer" + } } @Test fun `when computer selects rock, and user selects paper, then user wins`() { - "paper".enterAsUserInput() + "paper".enterUserInput() runGameWithComputerChoice("rock") - console.asString() shouldEndWith "win" + assertUserWin() } @Test fun `when computer selects paper, and user selects scissors, then user wins`() { - "scissors".enterAsUserInput() + "scissors".enterUserInput() runGameWithComputerChoice("paper") - console.asString() shouldEndWith "win" + assertUserWin() } @Test fun `when computer selects scissors, and user selects rock, then user wins`() { - "rock".enterAsUserInput() + "rock".enterUserInput() runGameWithComputerChoice("scissors") - console.asString() shouldEndWith "win" + assertUserWin() } @Test fun `when computer selects rock, and user selects scissors, then user loses`() { - "scissors".enterAsUserInput() + "scissors".enterUserInput() runGame { "rock" } - console.asString() shouldEndWith "lose" + assertUserLoss() } @Test fun `when computer selects paper, and user selects rock, then user loses`() { - "rock".enterAsUserInput() - runGameWithComputerChoice("paper") - console.asString() shouldEndWith "lose" + "rock".enterUserInput() + runGameWithComputerChoice( "paper") + assertUserLoss() } @Test fun `when computer selects scissors, and user selects paper, then user loses`() { - "paper".enterAsUserInput() + "paper".enterUserInput() runGameWithComputerChoice("scissors") - console.asString() shouldEndWith "lose" + assertUserLoss() + } + + @Test + fun `when computer selects scissors, and user selects scissors, then user draws`() = with("scissors") { + enterUserInput() + runGameWithComputerChoice(this) + assertUserDraw() + } + + @Test + fun `when computer selects rock, and user selects rock, then user draws`() = with("rock") { + enterUserInput() + runGameWithComputerChoice(this) + assertUserDraw() + } + + + @Test + fun `when computer selects paper, and user selects paper, then user draws`() = with("paper") { + enterUserInput() + runGameWithComputerChoice(this) + assertUserDraw() } private fun runGameWithComputerChoice(choice: String) = runGame { choice } private fun runGame(rnd: (List) -> String = { it.random() }) { - Game(rnd)() + Game(rnd, input, output)() } - private fun String.enterAsUserInput() = - System.setIn(ByteArrayInputStream(toByteArray())) + private fun String.enterUserInput() = + every { input.line() } returns this + + private fun verifyOutput(block: (String) -> Unit) { + verify { output.display(withArg { block(it) }) } + } + + private fun assertUserWin() = + verifyOutput { it shouldEndWith "win" } + + private fun assertUserLoss() = + verifyOutput { it shouldEndWith "lose" } - private fun ByteArrayOutputStream.asString() = - toString().trim() + private fun assertUserDraw() = + verifyOutput { it shouldEndWith "draw" } } diff --git a/src/test/kotlin/RepeatGamesTest.kt b/src/test/kotlin/RepeatGamesTest.kt new file mode 100644 index 0000000..5a3ef14 --- /dev/null +++ b/src/test/kotlin/RepeatGamesTest.kt @@ -0,0 +1,83 @@ +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldEndWith +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream + +class RepeatGamesTest { + + private lateinit var input: UserInput + private lateinit var output: UserOutput + private lateinit var game: Game + private lateinit var repeat: RepeatGames + + @BeforeEach + fun setUp() { + input = mockk() + every { input.line() } returns "" + + output = mockk() + every { output.display(any()) } just runs + + game = mockk() + every { game() } just runs + + repeat = RepeatGames(output, input) { game } + } + + @Test + fun `given game finished, when user enters n on keyboard, then run one game`() { + "n".enterUserInput() + repeat.start() + verify(exactly = 1) { game() } + } + + @Test + fun `given game finished, when user enters no on keyboard, then run one game`() { + "no".enterUserInput() + repeat.start() + verify(exactly = 1) { game() } + } + + @Test + fun `given game finished, when user requests another game, and then enters n on keyboard, then run two games `() { + listOf("y", "n").enterUserInput() + repeat.start() + verify(exactly = 2) { game() } + } + + @Test + fun `given game finished, when user requests another game with no input, and then enters n on keyboard, then run two games `() { + listOf("", "n").enterUserInput() + repeat.start() + verify(exactly = 2) { game() } + } + + @Test + fun `when game finished, then show next game prompt`() { + "n".enterUserInput() + repeat.start() + verify { output.display(withArg { + it shouldBe "Play another game (Enter 'n' or 'no' to exit)?" + }) } + } + + @Test + fun `given two games, when games finished, then show next game prompt appears twice`() { + listOf("yes", "no").enterUserInput() + repeat.start() + verify(exactly = 2) { output.display(withArg { + it shouldBe "Play another game (Enter 'n' or 'no' to exit)?" + }) } + } + + private fun String.enterUserInput() = + listOf(this).enterUserInput() + private fun List.enterUserInput() = + every { input.line() } returnsMany this + +}