├── LICENSE ├── README.md ├── chapter04 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ └── Wordz.java │ └── test │ └── java │ └── com │ └── wordz │ └── PlaceholderTest.java ├── chapter05 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ ├── Wordz.java │ │ └── domain │ │ ├── Letter.java │ │ ├── Score.java │ │ └── Word.java │ └── test │ └── java │ └── com │ └── wordz │ └── domain │ └── WordTest.java ├── chapter06 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ ├── Wordz.java │ │ └── domain │ │ ├── Letter.java │ │ ├── Score.java │ │ └── Word.java │ └── test │ └── java │ └── com │ └── wordz │ └── domain │ └── WordTest.java ├── chapter07 ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ └── java │ └── shapes │ ├── ConsoleGraphics.java │ ├── Graphics.java │ ├── Rectangle.java │ ├── Shape.java │ ├── Shapes.java │ ├── ShapesDemo.java │ └── TextBox.java ├── chapter08 ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ ├── com │ │ └── wordz │ │ │ ├── Wordz.java │ │ │ └── domain │ │ │ ├── Letter.java │ │ │ ├── RandomNumbers.java │ │ │ ├── Score.java │ │ │ ├── Word.java │ │ │ ├── WordRepository.java │ │ │ ├── WordRepositoryException.java │ │ │ ├── WordSelection.java │ │ │ └── WordSelectionException.java │ │ └── examples │ │ ├── BatteryMonitor.java │ │ ├── DeviceApi.java │ │ ├── DiceRoll.java │ │ ├── DiceRollApp.java │ │ ├── MailServer.java │ │ ├── NotificationFailureException.java │ │ ├── RandomNumbers.java │ │ ├── RandomlyGeneratedNumbers.java │ │ ├── UserGreeting.java │ │ ├── UserId.java │ │ ├── UserNotifications.java │ │ └── UserProfiles.java │ └── test │ └── java │ ├── com │ └── wordz │ │ └── domain │ │ ├── WordSelectionFailureTest.java │ │ ├── WordSelectionTest.java │ │ └── WordTest.java │ └── examples │ ├── DiceRollTest.java │ ├── MockMailServer.java │ ├── StubRandomNumbers.java │ ├── UserGreetingTest.java │ └── WelcomeEmailTest.java ├── chapter09 ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ ├── Wordz.java │ │ └── domain │ │ ├── Letter.java │ │ ├── RandomNumbers.java │ │ ├── Score.java │ │ ├── Word.java │ │ ├── WordRepository.java │ │ └── WordSelection.java │ └── test │ └── java │ └── com │ └── wordz │ └── domain │ ├── WordSelectionTest.java │ └── WordTest.java ├── chapter10 ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ ├── Wordz.java │ │ ├── adapters │ │ └── db │ │ │ └── WordRepositoryPostgres.java │ │ └── domain │ │ ├── Letter.java │ │ ├── RandomNumbers.java │ │ ├── Score.java │ │ ├── Word.java │ │ ├── WordRepository.java │ │ └── WordSelection.java │ └── test │ ├── java │ └── com │ │ └── wordz │ │ ├── adapters │ │ └── db │ │ │ └── WordRepositoryPostgresTest.java │ │ └── domain │ │ ├── WordSelectionTest.java │ │ └── WordTest.java │ └── resources │ └── adapters │ └── data │ └── wordTable.json ├── chapter13 ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ └── domain │ │ ├── Game.java │ │ ├── GameRepository.java │ │ ├── GuessResult.java │ │ ├── Letter.java │ │ ├── Player.java │ │ ├── RandomNumbers.java │ │ ├── Score.java │ │ ├── Word.java │ │ ├── WordRepository.java │ │ ├── WordSelection.java │ │ └── Wordz.java │ └── test │ └── java │ └── com │ └── wordz │ └── domain │ ├── GuessTest.java │ ├── NewGameTest.java │ ├── WordSelectionTest.java │ └── WordTest.java ├── chapter14 ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── wordz │ │ ├── adapters │ │ └── db │ │ │ ├── GameRepositoryPostgres.java │ │ │ └── WordRepositoryPostgres.java │ │ └── domain │ │ ├── Game.java │ │ ├── GameRepository.java │ │ ├── GuessResult.java │ │ ├── Letter.java │ │ ├── Player.java │ │ ├── RandomNumbers.java │ │ ├── Score.java │ │ ├── Word.java │ │ ├── WordRepository.java │ │ ├── WordSelection.java │ │ └── Wordz.java │ └── test │ ├── java │ └── com │ │ └── wordz │ │ ├── adapters │ │ └── db │ │ │ ├── GameRepositoryPostgresTest.java │ │ │ ├── PostgresTestDataSource.java │ │ │ └── WordRepositoryPostgresTest.java │ │ └── domain │ │ ├── GuessTest.java │ │ ├── NewGameTest.java │ │ ├── PlayerTest.java │ │ ├── WordSelectionTest.java │ │ └── WordTest.java │ └── resources │ └── adapters │ └── data │ ├── createGame.json │ ├── emptyGame.json │ ├── threeWords.json │ ├── updatedGame.json │ └── wordTable.json ├── chapter15-plus-flyway-support ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── wordz │ │ │ ├── ProductionRandomNumbers.java │ │ │ ├── WordzApplication.java │ │ │ ├── WordzConfiguration.java │ │ │ ├── adapters │ │ │ ├── api │ │ │ │ ├── GuessHttpResponse.java │ │ │ │ ├── GuessHttpResponseMapper.java │ │ │ │ ├── GuessRequest.java │ │ │ │ └── WordzEndpoint.java │ │ │ └── db │ │ │ │ ├── DatabaseMigration.java │ │ │ │ ├── GameRepositoryPostgres.java │ │ │ │ └── WordRepositoryPostgres.java │ │ │ └── domain │ │ │ ├── Game.java │ │ │ ├── GameRepository.java │ │ │ ├── GuessResult.java │ │ │ ├── Letter.java │ │ │ ├── Player.java │ │ │ ├── RandomNumbers.java │ │ │ ├── Score.java │ │ │ ├── Word.java │ │ │ ├── WordRepository.java │ │ │ ├── WordSelection.java │ │ │ └── Wordz.java │ └── resources │ │ └── db │ │ └── postgres │ │ └── V2__initial_schema.sql │ └── test │ ├── java │ └── com │ │ └── wordz │ │ ├── WordzConfigurationTest.java │ │ ├── adapters │ │ ├── api │ │ │ ├── GuessHttpResponseMapperTest.java │ │ │ └── WordzEndpointTest.java │ │ └── db │ │ │ ├── DatabaseMigrationPostgresTest.java │ │ │ ├── GameRepositoryPostgresTest.java │ │ │ ├── PostgresTestDataSource.java │ │ │ └── WordRepositoryPostgresTest.java │ │ └── domain │ │ ├── GuessTest.java │ │ ├── NewGameTest.java │ │ ├── PlayerTest.java │ │ ├── WordSelectionTest.java │ │ └── WordTest.java │ └── resources │ └── adapters │ └── data │ ├── createGame.json │ ├── emptyGame.json │ ├── initialSchema.json │ ├── threeWords.json │ ├── updatedGame.json │ └── wordTable.json └── chapter15 ├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── wordz │ ├── ProductionRandomNumbers.java │ ├── WordzApplication.java │ ├── WordzConfiguration.java │ ├── adapters │ ├── api │ │ ├── GuessHttpResponse.java │ │ ├── GuessHttpResponseMapper.java │ │ ├── GuessRequest.java │ │ └── WordzEndpoint.java │ └── db │ │ ├── GameRepositoryPostgres.java │ │ └── WordRepositoryPostgres.java │ └── domain │ ├── Game.java │ ├── GameRepository.java │ ├── GuessResult.java │ ├── Letter.java │ ├── Player.java │ ├── RandomNumbers.java │ ├── Score.java │ ├── Word.java │ ├── WordRepository.java │ ├── WordSelection.java │ └── Wordz.java └── test ├── java └── com │ └── wordz │ ├── WordzConfigurationTest.java │ ├── adapters │ ├── api │ │ ├── GuessHttpResponseMapperTest.java │ │ └── WordzEndpointTest.java │ └── db │ │ ├── GameRepositoryPostgresTest.java │ │ ├── PostgresTestDataSource.java │ │ └── WordRepositoryPostgresTest.java │ └── domain │ ├── GuessTest.java │ ├── NewGameTest.java │ ├── PlayerTest.java │ ├── WordSelectionTest.java │ └── WordTest.java └── resources └── adapters └── data ├── createGame.json ├── emptyGame.json ├── threeWords.json ├── updatedGame.json └── wordTable.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chapter04/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | } 17 | 18 | test { 19 | useJUnitPlatform() 20 | } -------------------------------------------------------------------------------- /chapter04/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter04/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter04/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter04/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter04/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter04/src/test/java/com/wordz/PlaceholderTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | @Deprecated 4 | public class PlaceholderTest { 5 | // Not implemented 6 | } 7 | -------------------------------------------------------------------------------- /chapter05/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | } 17 | 18 | test { 19 | useJUnitPlatform() 20 | } -------------------------------------------------------------------------------- /chapter05/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter05/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter05/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter05/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter05/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter05/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter05/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter05/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Score { 4 | public Letter letter(int position) { 5 | return Letter.INCORRECT; 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /chapter05/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | public Word(String correctWord) { 5 | // Not Implemented 6 | } 7 | 8 | public Score guess(String attempt) { 9 | var score = new Score(); 10 | return score; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /chapter05/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | 10 | @Test 11 | public void oneIncorrectLetter() { 12 | var word = new Word("A"); 13 | 14 | var score = word.guess("Z"); 15 | 16 | assertThat( score.letter(0) ).isEqualTo(Letter.INCORRECT); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter06/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | } 17 | 18 | test { 19 | useJUnitPlatform() 20 | } -------------------------------------------------------------------------------- /chapter06/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter06/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter06/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter06/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter06/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter06/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter06/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter06/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class Score { 8 | private final String correct; 9 | private final List results = new ArrayList<>(); 10 | private int position; 11 | 12 | public Score(String correct) { 13 | this.correct = correct; 14 | } 15 | 16 | public Letter letter(int position) { 17 | return results.get(position); 18 | } 19 | 20 | public void assess(String attempt) { 21 | for (char current: attempt.toCharArray()) { 22 | results.add( scoreFor(current) ); 23 | position++; 24 | } 25 | } 26 | 27 | private Letter scoreFor(char current) { 28 | if (isCorrectLetter(current)) { 29 | return Letter.CORRECT; 30 | } 31 | 32 | if (occursInWord(current)) { 33 | return Letter.PART_CORRECT; 34 | } 35 | 36 | return Letter.INCORRECT; 37 | } 38 | 39 | private boolean occursInWord(char current) { 40 | return correct.contains(String.valueOf(current)); 41 | } 42 | 43 | private boolean isCorrectLetter(char currentLetter) { 44 | return correct.charAt(position) == currentLetter; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter06/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter06/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | 41 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 42 | for (int position = 0; position < expectedScores.length; position++) { 43 | Letter expected = expectedScores[position]; 44 | assertThat(score.letter(position)) 45 | .isEqualTo(expected); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter07/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter07/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chapter07/README.md: -------------------------------------------------------------------------------- 1 | # Test-Driven-Development-with-Java 2 | Test-Driven Development with Java 3 | 4 | 5 | Demonstration of the SOLID principles, using the familiar OO example of drawing shapes. 6 | 7 | ## Running the demo 8 | Run ShapesDemo.main() 9 | -------------------------------------------------------------------------------- /chapter07/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.6.1' 17 | } 18 | 19 | test { 20 | useJUnitPlatform() 21 | } -------------------------------------------------------------------------------- /chapter07/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter07/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter07/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter07/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter07/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/ConsoleGraphics.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public class ConsoleGraphics implements Graphics { 4 | @Override 5 | public void drawText(String text) { 6 | print(text); 7 | } 8 | 9 | @Override 10 | public void drawHorizontalLine(int width) { 11 | var rowText = new StringBuilder(); 12 | 13 | for (int i = 0; i < width; i++) { 14 | rowText.append('X'); 15 | } 16 | 17 | print(rowText.toString()); 18 | } 19 | 20 | private void print(String text) { 21 | System.out.println(text); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/Graphics.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public interface Graphics { 4 | void drawText(String text); 5 | 6 | void drawHorizontalLine(int width); 7 | } 8 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/Rectangle.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public class Rectangle implements Shape { 4 | private final int width; 5 | private final int height; 6 | 7 | public Rectangle(int width, int height){ 8 | this.width = width; 9 | this.height = height; 10 | } 11 | 12 | @Override 13 | public void draw(Graphics g) { 14 | for (int row=0; row < height; row++) { 15 | g.drawHorizontalLine(width); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/Shape.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public interface Shape { 4 | void draw(Graphics g); 5 | } 6 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/Shapes.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Shapes { 7 | private final List all = new ArrayList<>(); 8 | private final Graphics graphics; 9 | 10 | public Shapes(Graphics graphics) { 11 | this.graphics = graphics; 12 | } 13 | 14 | public void add(Shape s) { 15 | all.add(s); 16 | } 17 | 18 | public void draw() { 19 | all.forEach(shape->shape.draw(graphics)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/ShapesDemo.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public class ShapesDemo { 4 | public static void main(String[] args) { 5 | new ShapesDemo().run(); 6 | } 7 | 8 | private void run() { 9 | // LSP: Implement the Graphics interface making it LSP compatible 10 | Graphics console = new ConsoleGraphics(); 11 | 12 | // DIP: Inject Shapes dependency on Graphics 13 | var shapes = new Shapes(console); 14 | 15 | // OCP Shapes class can have any kind of Shape added to it 16 | // SRP each shape subclass eg Rectangle knows how to draw only one shape 17 | // LSP each shape subclass can be used wherever a Shape interface is needed 18 | shapes.add(new TextBox("Hello from the Shapes SOLID Demo")); 19 | shapes.add(new Rectangle(32,1)); 20 | shapes.add(new TextBox("Using the SOLID principles to")); 21 | shapes.add(new TextBox("create an extensible mini-framework.")); 22 | shapes.add(new TextBox("Draw shapes as ASCII art.")); 23 | shapes.add(new TextBox("Following is a 5 x 3 Rectangle:")); 24 | shapes.add(new Rectangle(5,3)); 25 | 26 | // Tell Don't Ask principle: shapes, you know what to do: draw yourselves 27 | shapes.draw(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chapter07/src/main/java/shapes/TextBox.java: -------------------------------------------------------------------------------- 1 | package shapes; 2 | 3 | public class TextBox implements Shape { 4 | private final String text; 5 | 6 | public TextBox(String text) { 7 | this.text = text; 8 | } 9 | 10 | @Override 11 | public void draw(Graphics g) { 12 | g.drawText(text); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /chapter08/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter08/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.8.0' 17 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 18 | testImplementation 'com.github.database-rider:rider-core:1.33.0' 19 | testImplementation 'com.github.database-rider:rider-junit5:1.33.0' 20 | 21 | implementation 'org.postgresql:postgresql:42.5.0' 22 | } 23 | 24 | test { 25 | useJUnitPlatform() 26 | } -------------------------------------------------------------------------------- /chapter08/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter08/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter08/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter08/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter08/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class Score { 8 | private final String correct; 9 | private final List results = new ArrayList<>(); 10 | private int position; 11 | 12 | public Score(String correct) { 13 | this.correct = correct; 14 | } 15 | 16 | public Letter letter(int position) { 17 | return results.get(position); 18 | } 19 | 20 | public void assess(String attempt) { 21 | for (char current: attempt.toCharArray()) { 22 | results.add( scoreFor(current) ); 23 | position++; 24 | } 25 | } 26 | 27 | private Letter scoreFor(char current) { 28 | if (isCorrectLetter(current)) { 29 | return Letter.CORRECT; 30 | } 31 | 32 | if (occursInWord(current)) { 33 | return Letter.PART_CORRECT; 34 | } 35 | 36 | return Letter.INCORRECT; 37 | } 38 | 39 | private boolean occursInWord(char current) { 40 | return correct.contains(String.valueOf(current)); 41 | } 42 | 43 | private boolean isCorrectLetter(char currentLetter) { 44 | return correct.charAt(position) == currentLetter; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/WordRepositoryException.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordRepositoryException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | try { 14 | int wordNumber = random.next(repository.highestWordNumber()); 15 | return repository.fetchWordByNumber(wordNumber); 16 | } catch (WordRepositoryException wre){ 17 | throw new WordSelectionException("Could not select word", wre); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /chapter08/src/main/java/com/wordz/domain/WordSelectionException.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelectionException extends RuntimeException { 4 | public WordSelectionException(String reason, Throwable t) { 5 | super(reason, t); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/BatteryMonitor.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class BatteryMonitor { 4 | public void warnWhenBatteryPowerLow() { 5 | if ( DeviceApi.getBatteryPercentage() < 10 ) { 6 | System.out.println("Warning - Battery low"); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/DeviceApi.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class DeviceApi { 4 | public static int getBatteryPercentage() { 5 | return 9; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/DiceRoll.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class DiceRoll { 4 | private final int NUMBER_OF_SIDES = 6; 5 | private final RandomNumbers rnd; 6 | 7 | public DiceRoll(RandomNumbers r) { 8 | this.rnd = r; 9 | } 10 | 11 | public String asText() { 12 | int rolled = rnd.nextInt(NUMBER_OF_SIDES) + 1; 13 | 14 | return String.format("You rolled a %d", rolled); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/DiceRollApp.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class DiceRollApp { 4 | public static void main(String[] args) { 5 | new DiceRollApp().run(); 6 | } 7 | 8 | private void run() { 9 | var rnd = new RandomlyGeneratedNumbers(); 10 | var roll = new DiceRoll(rnd); 11 | 12 | System.out.println(roll.asText()); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/MailServer.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public interface MailServer { 4 | void sendEmail(String recipient, String subject, String text); 5 | } 6 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/NotificationFailureException.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class NotificationFailureException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public interface RandomNumbers { 4 | int nextInt(int upperBoundExclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/RandomlyGeneratedNumbers.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import examples.RandomNumbers; 4 | 5 | import java.util.random.RandomGenerator; 6 | 7 | public class RandomlyGeneratedNumbers implements RandomNumbers { 8 | @Override 9 | public int nextInt(int upperBoundExclusive) { 10 | var rnd = RandomGenerator.getDefault(); 11 | return rnd.nextInt(upperBoundExclusive); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/UserGreeting.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class UserGreeting { 4 | private final UserProfiles profiles ; 5 | 6 | public UserGreeting(UserProfiles profiles) { 7 | this.profiles = profiles; 8 | } 9 | 10 | public String formatGreeting(UserId id) { 11 | return String.format("Hello and welcome, %s", 12 | profiles.fetchNicknameFor(id)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/UserId.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import java.util.Objects; 4 | 5 | public class UserId { 6 | private long id; 7 | 8 | public UserId(long id) { 9 | this.id = id; 10 | } 11 | 12 | @Override 13 | public boolean equals(Object o) { 14 | if (this == o) return true; 15 | if (o == null || getClass() != o.getClass()) return false; 16 | UserId userId = (UserId) o; 17 | return id == userId.id; 18 | } 19 | 20 | @Override 21 | public int hashCode() { 22 | return Objects.hash(id); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/UserNotifications.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class UserNotifications { 4 | private final MailServer mailServer; 5 | 6 | public UserNotifications(MailServer mailServer) { 7 | this.mailServer = mailServer; 8 | } 9 | 10 | public void welcomeNewUser(String emailAddress) { 11 | try { 12 | mailServer.sendEmail(emailAddress, "Welcome!", "Welcome to your account"); 13 | } catch (IllegalArgumentException iae) { 14 | throw new NotificationFailureException(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter08/src/main/java/examples/UserProfiles.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public interface UserProfiles { 4 | String fetchNicknameFor(UserId id); 5 | } 6 | -------------------------------------------------------------------------------- /chapter08/src/test/java/com/wordz/domain/WordSelectionFailureTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 9 | import static org.mockito.ArgumentMatchers.anyInt; 10 | import static org.mockito.Mockito.doThrow; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionFailureTest { 14 | @Mock 15 | private WordRepository repository; 16 | 17 | @Mock 18 | private RandomNumbers random; 19 | 20 | @Test 21 | void reportsWordNotFound() { 22 | doThrow(new WordRepositoryException()) 23 | .when(repository) 24 | .fetchWordByNumber(anyInt()); 25 | 26 | var selection = new WordSelection(repository, 27 | random); 28 | 29 | assertThatExceptionOfType( 30 | WordSelectionException.class) 31 | .isThrownBy( 32 | ()->selection.chooseRandomWord()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter08/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.mockito.Mockito.when; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | public class WordSelectionTest { 13 | 14 | private static final int HIGHEST_WORD_NUMBER = 3; 15 | private static final int WORD_NUMBER_SHINE = 2; 16 | 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @Test 24 | void selectsWordAtRandom() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | 29 | var selector = new WordSelection(repository, random); 30 | 31 | String actual = selector.chooseRandomWord(); 32 | 33 | assertThat(actual).isEqualTo("SHINE"); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /chapter08/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | 41 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 42 | for (int position = 0; position < expectedScores.length; position++) { 43 | Letter expected = expectedScores[position]; 44 | assertThat(score.letter(position)) 45 | .isEqualTo(expected); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter08/src/test/java/examples/DiceRollTest.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.junit.jupiter.MockitoExtension; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | @ExtendWith(MockitoExtension.class) 10 | class DiceRollTest { 11 | 12 | @Test 13 | void producesMessage() { 14 | var stub = new StubRandomNumbers(); 15 | var roll = new DiceRoll(stub); 16 | 17 | var actual = roll.asText(); 18 | 19 | assertThat(actual).isEqualTo("You rolled a 5"); 20 | } 21 | } -------------------------------------------------------------------------------- /chapter08/src/test/java/examples/MockMailServer.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import examples.MailServer; 4 | 5 | public class MockMailServer implements MailServer { 6 | boolean wasCalled; 7 | String actualRecipient; 8 | String actualSubject; 9 | String actualText; 10 | 11 | @Override 12 | public void sendEmail(String recipient, String subject, String text) { 13 | wasCalled = true; 14 | actualRecipient = recipient; 15 | actualSubject = subject; 16 | actualText = text; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter08/src/test/java/examples/StubRandomNumbers.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class StubRandomNumbers implements RandomNumbers { 4 | @Override 5 | public int nextInt(int upperBoundExclusive) { 6 | return 4; // @see https://xkcd.com/221 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter08/src/test/java/examples/UserGreetingTest.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.mockito.Mockito.when; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | class UserGreetingTest { 13 | private static final UserId USER_ID = new UserId(1234); 14 | @Mock 15 | private UserProfiles profiles; 16 | 17 | @Test 18 | void formatsGreetingWithName() { 19 | when(profiles.fetchNicknameFor(USER_ID)) 20 | .thenReturn("Alan"); 21 | var greetings = new UserGreeting(profiles); 22 | 23 | String actual = greetings.formatGreeting(USER_ID); 24 | 25 | assertThat(actual).isEqualTo("Hello and welcome, Alan"); 26 | } 27 | } -------------------------------------------------------------------------------- /chapter08/src/test/java/examples/WelcomeEmailTest.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 9 | import static org.mockito.ArgumentMatchers.any; 10 | import static org.mockito.Mockito.doThrow; 11 | import static org.mockito.Mockito.verify; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | class WelcomeEmailTest { 15 | @Mock 16 | private MailServer mailServer; 17 | 18 | @Test 19 | public void sendsWelcomeEmail() { 20 | var notifications 21 | = new UserNotifications( mailServer ); 22 | 23 | notifications.welcomeNewUser("test@example.com"); 24 | 25 | verify(mailServer).sendEmail("test@example.com", 26 | "Welcome!", 27 | "Welcome to your account"); 28 | } 29 | 30 | 31 | @Test 32 | public void rejectsInvalidEmailRecipient() { 33 | doThrow(new IllegalArgumentException()) 34 | .when(mailServer).sendEmail(any(),any(),any()); 35 | 36 | var notifications 37 | = new UserNotifications( mailServer ); 38 | 39 | assertThatExceptionOfType(NotificationFailureException.class) 40 | .isThrownBy(()->notifications 41 | .welcomeNewUser("not-an-email-address")); 42 | } 43 | } -------------------------------------------------------------------------------- /chapter09/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter09/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.8.0' 17 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } -------------------------------------------------------------------------------- /chapter09/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter09/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter09/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter09/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter09/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class Score { 8 | private final String correct; 9 | private final List results = new ArrayList<>(); 10 | private int position; 11 | 12 | public Score(String correct) { 13 | this.correct = correct; 14 | } 15 | 16 | public Letter letter(int position) { 17 | return results.get(position); 18 | } 19 | 20 | public void assess(String attempt) { 21 | for (char current: attempt.toCharArray()) { 22 | results.add( scoreFor(current) ); 23 | position++; 24 | } 25 | } 26 | 27 | private Letter scoreFor(char current) { 28 | if (isCorrectLetter(current)) { 29 | return Letter.CORRECT; 30 | } 31 | 32 | if (occursInWord(current)) { 33 | return Letter.PART_CORRECT; 34 | } 35 | 36 | return Letter.INCORRECT; 37 | } 38 | 39 | private boolean occursInWord(char current) { 40 | return correct.contains(String.valueOf(current)); 41 | } 42 | 43 | private boolean isCorrectLetter(char currentLetter) { 44 | return correct.charAt(position) == currentLetter; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter09/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter09/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter09/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | 41 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 42 | for (int position = 0; position < expectedScores.length; position++) { 43 | Letter expected = expectedScores[position]; 44 | assertThat(score.letter(position)) 45 | .isEqualTo(expected); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter10/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter10/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.8.0' 17 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 18 | testImplementation 'com.github.database-rider:rider-core:1.33.0' 19 | testImplementation 'com.github.database-rider:rider-junit5:1.33.0' 20 | 21 | implementation 'org.postgresql:postgresql:42.5.0' 22 | } 23 | 24 | test { 25 | useJUnitPlatform() 26 | } -------------------------------------------------------------------------------- /chapter10/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter10/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter10/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter10/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter10/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | public class Wordz { 4 | public static void main(String[] args) { 5 | // Not implemented 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/adapters/db/WordRepositoryPostgres.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.wordz.domain.WordRepository; 4 | 5 | import javax.sql.DataSource; 6 | 7 | public class WordRepositoryPostgres implements WordRepository { 8 | public WordRepositoryPostgres(DataSource ds) { 9 | } 10 | 11 | @Override 12 | public String fetchWordByNumber(int number) { 13 | throw new UnsupportedOperationException("Not implemented"); 14 | } 15 | 16 | @Override 17 | public int highestWordNumber() { 18 | throw new UnsupportedOperationException("Not implemented"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class Score { 8 | private final String correct; 9 | private final List results = new ArrayList<>(); 10 | private int position; 11 | 12 | public Score(String correct) { 13 | this.correct = correct; 14 | } 15 | 16 | public Letter letter(int position) { 17 | return results.get(position); 18 | } 19 | 20 | public void assess(String attempt) { 21 | for (char current: attempt.toCharArray()) { 22 | results.add( scoreFor(current) ); 23 | position++; 24 | } 25 | } 26 | 27 | private Letter scoreFor(char current) { 28 | if (isCorrectLetter(current)) { 29 | return Letter.CORRECT; 30 | } 31 | 32 | if (occursInWord(current)) { 33 | return Letter.PART_CORRECT; 34 | } 35 | 36 | return Letter.INCORRECT; 37 | } 38 | 39 | private boolean occursInWord(char current) { 40 | return correct.contains(String.valueOf(current)); 41 | } 42 | 43 | private boolean isCorrectLetter(char currentLetter) { 44 | return correct.charAt(position) == currentLetter; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter10/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter10/src/test/java/com/wordz/adapters/db/WordRepositoryPostgresTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.github.database.rider.core.api.connection.ConnectionHolder; 4 | import com.github.database.rider.core.api.dataset.DataSet; 5 | import com.github.database.rider.junit5.api.DBRider; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.api.Test; 9 | import org.postgresql.ds.PGSimpleDataSource; 10 | 11 | import javax.sql.DataSource; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | @Disabled // Because Chapter 10 defers making this test pass until chapter14 16 | @DBRider 17 | public class WordRepositoryPostgresTest { 18 | private DataSource dataSource; 19 | 20 | @BeforeEach 21 | void beforeEachTest() { 22 | var ds = new PGSimpleDataSource(); 23 | ds.setServerNames(new String[]{"localhost"}); 24 | ds.setDatabaseName("wordzdb"); 25 | ds.setUser("ciuser"); 26 | ds.setPassword("cipassword"); 27 | 28 | this.dataSource = ds; 29 | } 30 | 31 | private final ConnectionHolder connectionHolder = () -> dataSource.getConnection(); 32 | 33 | @Test 34 | @DataSet("adapters/data/wordTable.json") 35 | public void fetchesWord() { 36 | var adapter = new WordRepositoryPostgres(dataSource); 37 | 38 | String actual = adapter.fetchWordByNumber(27); 39 | 40 | assertThat(actual).isEqualTo("ARISE"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /chapter10/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter10/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | 41 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 42 | for (int position = 0; position < expectedScores.length; position++) { 43 | Letter expected = expectedScores[position]; 44 | assertThat(score.letter(position)) 45 | .isEqualTo(expected); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter10/src/test/resources/adapters/data/wordTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "WORD": [ 3 | { 4 | "id": 1, 5 | "number": 27, 6 | "text": "ARISE" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /chapter13/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter13/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.8.0' 17 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } -------------------------------------------------------------------------------- /chapter13/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter13/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter13/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter13/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /chapter13/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Game.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Game { 4 | private static final int MAXIMUM_NUMBER_ALLOWED_GUESSES = 5; 5 | private final Player player; 6 | private final String targetWord; 7 | private int attemptNumber; 8 | private boolean isGameOver; 9 | 10 | Game(Player player, String targetWord, int attemptNumber, boolean isGameOver) { 11 | this.player = player; 12 | this.targetWord = targetWord; 13 | this.attemptNumber = attemptNumber; 14 | this.isGameOver = isGameOver; 15 | } 16 | 17 | static Game create(Player player, String correctWord) { 18 | return new Game(player, correctWord, 0, false); 19 | } 20 | 21 | static Game create(Player player, String correctWord, int attemptNumber) { 22 | return new Game(player, correctWord, attemptNumber, false); 23 | } 24 | 25 | public Player getPlayer() { 26 | return player; 27 | } 28 | 29 | public int getAttemptNumber() { 30 | return attemptNumber; 31 | } 32 | 33 | public String getWord() { 34 | return targetWord; 35 | } 36 | 37 | public Score attempt(String guess) { 38 | trackNumberOfAttempts(); 39 | 40 | var word = new Word(targetWord); 41 | Score score = word.guess(guess); 42 | 43 | if (score.allCorrect()) { 44 | end(); 45 | } 46 | 47 | return score; 48 | } 49 | 50 | private void trackNumberOfAttempts() { 51 | attemptNumber++; 52 | 53 | if (attemptNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES) { 54 | end(); 55 | } 56 | } 57 | 58 | public boolean isGameOver() { 59 | return isGameOver; 60 | } 61 | 62 | void end() { 63 | isGameOver = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/GameRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface GameRepository { 4 | void create(Game game); 5 | 6 | Game fetchForPlayer(Player player); 7 | 8 | void update(Game game); 9 | } 10 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/GuessResult.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public record GuessResult( 4 | Score score, 5 | boolean isGameOver, 6 | boolean isError 7 | ) { 8 | static final GuessResult ERROR = new GuessResult(null, true, true); 9 | 10 | static GuessResult create(Score score, boolean isGameOver) { 11 | return new GuessResult(score, isGameOver, false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Player.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Player { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.util.Collections.unmodifiableList; 7 | 8 | public class Score { 9 | private final String correct; 10 | private final List results = new ArrayList<>(); 11 | private int position; 12 | 13 | public Score(String correct) { 14 | this.correct = correct; 15 | } 16 | 17 | public Letter letter(int position) { 18 | return results.get(position); 19 | } 20 | 21 | public void assess(String attempt) { 22 | for (char current: attempt.toCharArray()) { 23 | results.add( scoreFor(current) ); 24 | position++; 25 | } 26 | } 27 | 28 | public boolean allCorrect() { 29 | var totalCorrect = results.stream() 30 | .filter(letter -> letter == Letter.CORRECT) 31 | .count(); 32 | 33 | return totalCorrect == results.size(); 34 | } 35 | 36 | public List letters() { 37 | return unmodifiableList(results); 38 | } 39 | 40 | private Letter scoreFor(char current) { 41 | if (isCorrectLetter(current)) { 42 | return Letter.CORRECT; 43 | } 44 | 45 | if (occursInWord(current)) { 46 | return Letter.PART_CORRECT; 47 | } 48 | 49 | return Letter.INCORRECT; 50 | } 51 | 52 | private boolean occursInWord(char current) { 53 | return correct.contains(String.valueOf(current)); 54 | } 55 | 56 | private boolean isCorrectLetter(char currentLetter) { 57 | return correct.charAt(position) == currentLetter; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter13/src/main/java/com/wordz/domain/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public class Wordz { 6 | private final GameRepository gameRepository; 7 | private final WordSelection wordSelection ; 8 | 9 | public Wordz(GameRepository gr, 10 | WordRepository wr, 11 | RandomNumbers rn) { 12 | this.gameRepository = gr; 13 | this.wordSelection = new WordSelection(wr, rn); 14 | } 15 | 16 | public void newGame(Player player) { 17 | var word = wordSelection.chooseRandomWord(); 18 | gameRepository.create(Game.create(player, word)); 19 | } 20 | 21 | public GuessResult assess(Player player, String guess) { 22 | Game game = gameRepository.fetchForPlayer(player); 23 | 24 | if(game.isGameOver()) { 25 | return GuessResult.ERROR; 26 | } 27 | 28 | Score score = game.attempt( guess ); 29 | 30 | gameRepository.update(game); 31 | 32 | return GuessResult.create(score, game.isGameOver()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter13/src/test/java/com/wordz/domain/GuessTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.mockito.Mockito.*; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | public class GuessTest { 15 | private static final Player PLAYER = new Player(); 16 | private static final String CORRECT_WORD = "ARISE"; 17 | private static final String WRONG_WORD = "RXXXX"; 18 | @Mock 19 | private GameRepository gameRepository; 20 | @InjectMocks 21 | private Wordz wordz; 22 | 23 | @Test 24 | void returnsScoreForGuess() { 25 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 26 | 27 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 28 | 29 | Letter firstLetter = result.score().letter(0); 30 | assertThat(firstLetter).isEqualTo(Letter.PART_CORRECT); 31 | } 32 | 33 | @Test 34 | void updatesAttemptNumber() { 35 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 36 | 37 | wordz.assess(PLAYER, WRONG_WORD); 38 | 39 | var game = getUpdatedGameInRepository(); 40 | assertThat(game.getAttemptNumber()).isEqualTo(1); 41 | } 42 | 43 | @Test 44 | void gameOverOnTooManyIncorrectGuesses(){ 45 | int maximumGuesses = 5; 46 | givenGameInRepository( 47 | Game.create(PLAYER, CORRECT_WORD, 48 | maximumGuesses-1)); 49 | 50 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 51 | 52 | assertThat(result.isGameOver()).isTrue(); 53 | } 54 | 55 | @Test 56 | void rejectsGuessAfterGameOver(){ 57 | var game = Game.create(PLAYER, CORRECT_WORD); 58 | game.end(); 59 | givenGameInRepository( game ); 60 | 61 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 62 | 63 | assertThat(result.isError()).isTrue(); 64 | } 65 | 66 | @Test 67 | void recordsGameOverOnCorrectGuess(){ 68 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 69 | 70 | wordz.assess(PLAYER, CORRECT_WORD); 71 | 72 | Game game = getUpdatedGameInRepository(); 73 | assertThat(game.isGameOver()).isTrue(); 74 | } 75 | 76 | private Game getUpdatedGameInRepository() { 77 | ArgumentCaptor argument = ArgumentCaptor.forClass(Game.class); 78 | verify(gameRepository).update(argument.capture()); 79 | return argument.getValue(); 80 | } 81 | 82 | private void givenGameInRepository(Game game) { 83 | when(gameRepository.fetchForPlayer(eq(PLAYER))) 84 | .thenReturn(game); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /chapter13/src/test/java/com/wordz/domain/NewGameTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.ArgumentCaptor; 7 | import org.mockito.Captor; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.ArgumentMatchers.anyInt; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class NewGameTest { 19 | private static final Player PLAYER = new Player(); 20 | @Mock 21 | private GameRepository gameRepository; 22 | @Mock 23 | private WordRepository wordRepository ; 24 | @Mock 25 | private RandomNumbers randomNumbers ; 26 | @InjectMocks 27 | private Wordz wordz; 28 | 29 | @Test 30 | void startsNewGame() { 31 | givenWordToSelect("ARISE"); 32 | 33 | wordz.newGame(PLAYER); 34 | 35 | Game game = getGameInRepository(); 36 | assertThat(game.getWord()).isEqualTo("ARISE"); 37 | assertThat(game.getAttemptNumber()).isZero(); 38 | assertThat(game.getPlayer()).isSameAs(PLAYER); 39 | } 40 | 41 | private void givenWordToSelect(String wordToSelect) { 42 | int wordNumber = 2; 43 | 44 | when(randomNumbers.next(anyInt())) 45 | .thenReturn(wordNumber); 46 | 47 | when(wordRepository 48 | .fetchWordByNumber(wordNumber)) 49 | .thenReturn(wordToSelect); 50 | } 51 | 52 | private Game getGameInRepository() { 53 | var gameArgument = ArgumentCaptor.forClass(Game.class); 54 | verify(gameRepository) 55 | .create(gameArgument.capture()); 56 | var game = gameArgument.getValue(); 57 | return game; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /chapter13/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter13/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | @Test 41 | void reportsAllCorrect() { 42 | var word = new Word("ARISE"); 43 | var score = word.guess("ARISE"); 44 | assertThat(score.allCorrect()).isTrue(); 45 | } 46 | 47 | @Test 48 | void reportsNotAllCorrect() { 49 | var word = new Word("ARISE"); 50 | var score = word.guess("ARI*E"); 51 | assertThat(score.allCorrect()).isFalse(); 52 | } 53 | 54 | @Test 55 | void accessesLetters() { 56 | var word = new Word("ARISE"); 57 | var score = word.guess("ARI*E"); 58 | assertThat(score.letters()).hasSize(5); 59 | } 60 | 61 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 62 | for (int position = 0; position < expectedScores.length; position++) { 63 | Letter expected = expectedScores[position]; 64 | assertThat(score.letter(position)) 65 | .isEqualTo(expected); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /chapter14/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/**/workspace.xml 70 | .idea/**/tasks.xml 71 | .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.xml 77 | .idea/**/dataSources.local.xml 78 | .idea/**/sqlDataSources.xml 79 | .idea/**/dynamic.xml 80 | .idea/**/uiDesigner.xml 81 | 82 | # Gradle: 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | ## File-based project format: 87 | *.iws 88 | 89 | ## Plugin-specific files: 90 | 91 | # IntelliJ 92 | /out/ 93 | 94 | # mpeltonen/sbt-idea plugin 95 | .idea_modules/ 96 | -------------------------------------------------------------------------------- /chapter14/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.mockito:mockito-core:4.8.0' 17 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 18 | testImplementation 'com.github.database-rider:rider-core:1.35.0' 19 | testImplementation 'com.github.database-rider:rider-junit5:1.35.0' 20 | 21 | implementation 'org.postgresql:postgresql:42.5.0' 22 | implementation 'org.jdbi:jdbi3-core:3.34.0' 23 | implementation 'org.apache.commons:commons-lang3:3.12.0' 24 | } 25 | 26 | test { 27 | useJUnitPlatform() 28 | } -------------------------------------------------------------------------------- /chapter14/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter14/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter14/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter14/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/adapters/db/WordRepositoryPostgres.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.wordz.domain.WordRepository; 4 | import org.jdbi.v3.core.Jdbi; 5 | 6 | import javax.sql.DataSource; 7 | 8 | public class WordRepositoryPostgres implements WordRepository { 9 | private static final String SQL_FETCH_WORD_BY_NUMBER 10 | = "select word from word where word_number=:wordNumber"; 11 | private static final String SQL_RETURN_HIGHEST_WORD_NUMBER 12 | = "select max(word_number) from word"; 13 | private final Jdbi jdbi; 14 | 15 | public WordRepositoryPostgres(DataSource dataSource) { 16 | jdbi = Jdbi.create(dataSource); 17 | } 18 | 19 | @Override 20 | public String fetchWordByNumber(int wordNumber) { 21 | String word = jdbi.withHandle(handle -> { 22 | var query = handle.createQuery(SQL_FETCH_WORD_BY_NUMBER); 23 | query.bind("wordNumber", wordNumber); 24 | 25 | return query 26 | .mapTo(String.class) 27 | .one(); 28 | }); 29 | 30 | return word ; 31 | } 32 | 33 | @Override 34 | public int highestWordNumber() { 35 | return jdbi.withHandle(handle-> 36 | handle.createQuery(SQL_RETURN_HIGHEST_WORD_NUMBER) 37 | .mapTo(Integer.class) 38 | .one()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Game.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Game { 4 | private static final int MAXIMUM_NUMBER_ALLOWED_GUESSES = 5; 5 | private final Player player; 6 | private final String targetWord; 7 | private int attemptNumber; 8 | private boolean isGameOver; 9 | 10 | public Game(Player player, String targetWord, int attemptNumber, boolean isGameOver) { 11 | this.player = player; 12 | this.targetWord = targetWord; 13 | this.attemptNumber = attemptNumber; 14 | this.isGameOver = isGameOver; 15 | } 16 | 17 | static Game create(Player player, String correctWord) { 18 | return new Game(player, correctWord, 0, false); 19 | } 20 | 21 | static Game create(Player player, String correctWord, int attemptNumber) { 22 | return new Game(player, correctWord, attemptNumber, false); 23 | } 24 | 25 | public Player getPlayer() { 26 | return player; 27 | } 28 | 29 | public int getAttemptNumber() { 30 | return attemptNumber; 31 | } 32 | 33 | public String getWord() { 34 | return targetWord; 35 | } 36 | 37 | public Score attempt(String guess) { 38 | trackNumberOfAttempts(); 39 | 40 | var word = new Word(targetWord); 41 | Score score = word.guess(guess); 42 | 43 | if (score.allCorrect()) { 44 | end(); 45 | } 46 | 47 | return score; 48 | } 49 | 50 | private void trackNumberOfAttempts() { 51 | attemptNumber++; 52 | 53 | if (attemptNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES) { 54 | end(); 55 | } 56 | } 57 | 58 | public boolean isGameOver() { 59 | return isGameOver; 60 | } 61 | 62 | void end() { 63 | isGameOver = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/GameRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface GameRepository { 6 | void create(Game game); 7 | 8 | Optional fetchForPlayer(Player player); 9 | 10 | void update(Game game); 11 | } 12 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/GuessResult.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public record GuessResult( 4 | Score score, 5 | boolean isGameOver, 6 | boolean isError 7 | ) { 8 | static final GuessResult ERROR = new GuessResult(null, true, true); 9 | 10 | static GuessResult create(Score score, boolean isGameOver) { 11 | return new GuessResult(score, isGameOver, false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Player.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 4 | import static org.apache.commons.lang3.builder.HashCodeBuilder.*; 5 | 6 | public class Player { 7 | private final String name; 8 | 9 | public Player(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object other) { 19 | return reflectionEquals(this, other); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return reflectionHashCode(this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.util.Collections.unmodifiableList; 7 | 8 | public class Score { 9 | private final String correct; 10 | private final List results = new ArrayList<>(); 11 | private int position; 12 | 13 | public Score(String correct) { 14 | this.correct = correct; 15 | } 16 | 17 | public Letter letter(int position) { 18 | return results.get(position); 19 | } 20 | 21 | public void assess(String attempt) { 22 | for (char current: attempt.toCharArray()) { 23 | results.add( scoreFor(current) ); 24 | position++; 25 | } 26 | } 27 | 28 | public boolean allCorrect() { 29 | var totalCorrect = results.stream() 30 | .filter(letter -> letter == Letter.CORRECT) 31 | .count(); 32 | 33 | return totalCorrect == results.size(); 34 | } 35 | 36 | public List letters() { 37 | return unmodifiableList(results); 38 | } 39 | 40 | private Letter scoreFor(char current) { 41 | if (isCorrectLetter(current)) { 42 | return Letter.CORRECT; 43 | } 44 | 45 | if (occursInWord(current)) { 46 | return Letter.PART_CORRECT; 47 | } 48 | 49 | return Letter.INCORRECT; 50 | } 51 | 52 | private boolean occursInWord(char current) { 53 | return correct.contains(String.valueOf(current)); 54 | } 55 | 56 | private boolean isCorrectLetter(char currentLetter) { 57 | return correct.charAt(position) == currentLetter; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter14/src/main/java/com/wordz/domain/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public class Wordz { 6 | private final GameRepository gameRepository; 7 | private final WordSelection selection ; 8 | 9 | public Wordz(GameRepository repository, WordRepository wordRepository, RandomNumbers randomNumbers) { 10 | this.gameRepository = repository; 11 | this.selection = new WordSelection(wordRepository, randomNumbers); 12 | } 13 | 14 | public boolean newGame(Player player) { 15 | Optional currentGame = gameRepository.fetchForPlayer(player); 16 | 17 | if (isGameInProgress(currentGame)) { 18 | return false ; 19 | } 20 | 21 | var word = selection.chooseRandomWord(); 22 | Game game = new Game(player, word, 0, false); 23 | gameRepository.create(game); 24 | return true; 25 | } 26 | 27 | public GuessResult assess(Player player, String guess) { 28 | Optional currentGame = gameRepository.fetchForPlayer(player); 29 | 30 | if (!isGameInProgress(currentGame)) { 31 | return GuessResult.ERROR; 32 | } 33 | 34 | return calculateScore(currentGame.get(), guess); 35 | } 36 | 37 | private GuessResult calculateScore(Game game, String guess) { 38 | Score score = game.attempt( guess ); 39 | 40 | gameRepository.update(game); 41 | return GuessResult.create(score, game.isGameOver()); 42 | } 43 | 44 | private boolean isGameInProgress(Optional currentGame) { 45 | if (currentGame.isEmpty()) { 46 | return false ; 47 | } 48 | 49 | return !currentGame.get().isGameOver(); 50 | } 51 | } -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/adapters/db/PostgresTestDataSource.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import org.postgresql.ds.PGSimpleDataSource; 4 | 5 | public class PostgresTestDataSource extends PGSimpleDataSource { 6 | PostgresTestDataSource () { 7 | setServerNames(new String[]{"localhost"}); 8 | setDatabaseName("wordzdb"); 9 | setCurrentSchema("public"); 10 | setUser("ciuser"); 11 | setPassword("cipassword"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/adapters/db/WordRepositoryPostgresTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.github.database.rider.core.api.configuration.DBUnit; 4 | import com.github.database.rider.core.api.configuration.Orthography; 5 | import com.github.database.rider.core.api.connection.ConnectionHolder; 6 | import com.github.database.rider.core.api.dataset.DataSet; 7 | import com.github.database.rider.junit5.api.DBRider; 8 | import com.wordz.domain.WordRepository; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import javax.sql.DataSource; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @DBRider 17 | @DBUnit(caseSensitiveTableNames = true, 18 | caseInsensitiveStrategy= Orthography.LOWERCASE) 19 | public class WordRepositoryPostgresTest { 20 | 21 | private DataSource dataSource; 22 | 23 | @SuppressWarnings("unused") // Used by DBRider framework 24 | private final ConnectionHolder connectionHolder = () -> dataSource.getConnection(); 25 | 26 | /** 27 | * As this method only sets up a connection to the database and does not 28 | * create any stored data, there is a good argument for using the @BeforeAll 29 | * annotation and doing this once for all tests in this class 30 | */ 31 | @BeforeEach 32 | void setupConnection() { 33 | this.dataSource = new PostgresTestDataSource(); 34 | } 35 | 36 | @Test 37 | @DataSet("adapters/data/wordTable.json") 38 | public void fetchesWord() { 39 | WordRepository repository = new WordRepositoryPostgres(dataSource); 40 | 41 | String actual = repository.fetchWordByNumber(27); 42 | 43 | assertThat(actual).isEqualTo("ARISE"); 44 | } 45 | 46 | @Test 47 | @DataSet("adapters/data/threeWords.json") 48 | public void returnsHighestWordNumber() { 49 | WordRepository repository = new WordRepositoryPostgres(dataSource); 50 | 51 | int actual = repository.highestWordNumber(); 52 | 53 | assertThat(actual).isEqualTo(3); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/domain/GuessTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.Optional; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.Mockito.*; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class GuessTest { 17 | private static final Player PLAYER = new Player("player1"); 18 | private static final String CORRECT_WORD = "ARISE"; 19 | private static final String WRONG_WORD = "RXXXX"; 20 | @Mock 21 | private GameRepository gameRepository; 22 | @InjectMocks 23 | private Wordz wordz; 24 | 25 | @Test 26 | void returnsScoreForGuess() { 27 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 28 | 29 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 30 | 31 | Letter firstLetter = result.score().letter(0); 32 | assertThat(firstLetter).isEqualTo(Letter.PART_CORRECT); 33 | } 34 | 35 | @Test 36 | void updatesAttemptNumber() { 37 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 38 | 39 | wordz.assess(PLAYER, WRONG_WORD); 40 | 41 | var game = getUpdatedGameInRepository(); 42 | assertThat(game.getAttemptNumber()).isEqualTo(1); 43 | } 44 | 45 | @Test 46 | void gameOverOnTooManyIncorrectGuesses(){ 47 | int maximumGuesses = 5; 48 | givenGameInRepository( 49 | Game.create(PLAYER, CORRECT_WORD, 50 | maximumGuesses-1)); 51 | 52 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 53 | 54 | assertThat(result.isGameOver()).isTrue(); 55 | } 56 | 57 | @Test 58 | void rejectsGuessAfterGameOver(){ 59 | var game = Game.create(PLAYER, CORRECT_WORD); 60 | game.end(); 61 | givenGameInRepository( game ); 62 | 63 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 64 | 65 | assertThat(result.isError()).isTrue(); 66 | } 67 | 68 | @Test 69 | void recordsGameOverOnCorrectGuess(){ 70 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 71 | 72 | wordz.assess(PLAYER, CORRECT_WORD); 73 | 74 | Game game = getUpdatedGameInRepository(); 75 | assertThat(game.isGameOver()).isTrue(); 76 | } 77 | 78 | private Game getUpdatedGameInRepository() { 79 | ArgumentCaptor argument = ArgumentCaptor.forClass(Game.class); 80 | verify(gameRepository).update(argument.capture()); 81 | return argument.getValue(); 82 | } 83 | 84 | private void givenGameInRepository(Game game) { 85 | when(gameRepository.fetchForPlayer(eq(PLAYER))) 86 | .thenReturn(Optional.of(game)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/domain/NewGameTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.*; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.mockito.ArgumentMatchers.anyInt; 13 | import static org.mockito.Mockito.*; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class NewGameTest { 19 | private static final Player PLAYER = new Player("player1"); 20 | private static final String CORRECT_WORD = "ARISE"; 21 | 22 | @Mock 23 | private GameRepository gameRepository; 24 | @Mock 25 | private WordRepository wordRepository ; 26 | @Mock 27 | private RandomNumbers randomNumbers ; 28 | @InjectMocks 29 | private Wordz wordz; 30 | 31 | @Test 32 | void startsNewGame() { 33 | givenWordToSelect(CORRECT_WORD); 34 | 35 | wordz.newGame(PLAYER); 36 | 37 | Game game = getGameInRepository(); 38 | assertThat(game.getWord()).isEqualTo(CORRECT_WORD); 39 | assertThat(game.getAttemptNumber()).isZero(); 40 | assertThat(game.getPlayer()).isSameAs(PLAYER); 41 | } 42 | 43 | @Test 44 | void cannotRestartGameInProgress() { 45 | when(gameRepository 46 | .fetchForPlayer(eq(PLAYER))) 47 | .thenReturn(Optional.of( 48 | Game.create(PLAYER, CORRECT_WORD))); 49 | var success = wordz.newGame(PLAYER); 50 | 51 | assertThat(success).isFalse(); 52 | } 53 | 54 | private void givenWordToSelect(String wordToSelect) { 55 | int wordNumber = 2; 56 | 57 | when(randomNumbers.next(anyInt())) 58 | .thenReturn(wordNumber); 59 | 60 | when(wordRepository 61 | .fetchWordByNumber(wordNumber)) 62 | .thenReturn(wordToSelect); 63 | } 64 | 65 | private Game getGameInRepository() { 66 | var gameArgument = ArgumentCaptor.forClass(Game.class); 67 | verify(gameRepository) 68 | .create(gameArgument.capture()); 69 | var game = gameArgument.getValue(); 70 | return game; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/domain/PlayerTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class PlayerTest { 8 | 9 | @Test 10 | void twoAreEqual() { 11 | Player a = new Player("same-name-means-equal"); 12 | Player b = new Player("same-name-means-equal"); 13 | 14 | assertThat(a).isEqualTo(b); 15 | } 16 | 17 | @Test 18 | void twoAreNotEqual() { 19 | Player a = new Player("same-name-means-equal"); 20 | Player b = new Player("different-name-means-not-equal"); 21 | 22 | assertThat(a).isNotEqualTo(b); 23 | } 24 | 25 | @Test 26 | void equalHaveSameHashcode() { 27 | Player a = new Player("same-name-means-equal"); 28 | Player b = new Player("same-name-means-equal"); 29 | 30 | assertThat(a.hashCode()).isEqualTo(b.hashCode()); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter14/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | @Test 41 | void reportsAllCorrect() { 42 | var word = new Word("ARISE"); 43 | var score = word.guess("ARISE"); 44 | assertThat(score.allCorrect()).isTrue(); 45 | } 46 | 47 | @Test 48 | void reportsNotAllCorrect() { 49 | var word = new Word("ARISE"); 50 | var score = word.guess("ARI*E"); 51 | assertThat(score.allCorrect()).isFalse(); 52 | } 53 | 54 | @Test 55 | void accessesLetters() { 56 | var word = new Word("ARISE"); 57 | var score = word.guess("ARI*E"); 58 | assertThat(score.letters()).hasSize(5); 59 | } 60 | 61 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 62 | for (int position = 0; position < expectedScores.length; position++) { 63 | Letter expected = expectedScores[position]; 64 | assertThat(score.letter(position)) 65 | .isEqualTo(expected); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /chapter14/src/test/resources/adapters/data/createGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 0, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter14/src/test/resources/adapters/data/emptyGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /chapter14/src/test/resources/adapters/data/threeWords.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 1, 5 | "word": "ARISE" 6 | }, 7 | { 8 | "word_number": 2, 9 | "word": "SHINE" 10 | }, 11 | { 12 | "word_number": 3, 13 | "word": "SLEEP" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /chapter14/src/test/resources/adapters/data/updatedGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 1, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter14/src/test/resources/adapters/data/wordTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 27, 5 | "word": "ARISE" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/ 70 | .idea/**/workspace.xml 71 | .idea/**/tasks.xml 72 | .idea/dictionaries 73 | 74 | # Sensitive or high-churn files: 75 | .idea/**/dataSources/ 76 | .idea/**/dataSources.ids 77 | .idea/**/dataSources.xml 78 | .idea/**/dataSources.local.xml 79 | .idea/**/sqlDataSources.xml 80 | .idea/**/dynamic.xml 81 | .idea/**/uiDesigner.xml 82 | 83 | # Gradle: 84 | .idea/**/gradle.xml 85 | .idea/**/libraries 86 | 87 | ## File-based project format: 88 | *.iws 89 | 90 | ## Plugin-specific files: 91 | 92 | # IntelliJ 93 | /out/ 94 | 95 | # mpeltonen/sbt-idea plugin 96 | .idea_modules/ 97 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.hamcrest:hamcrest:2.2' 17 | testImplementation 'org.mockito:mockito-core:4.8.0' 18 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 19 | testImplementation 'com.github.database-rider:rider-core:1.35.0' 20 | testImplementation 'com.github.database-rider:rider-junit5:1.35.0' 21 | 22 | implementation 'org.postgresql:postgresql:42.5.0' 23 | implementation 'org.jdbi:jdbi3-core:3.34.0' 24 | implementation 'org.apache.commons:commons-lang3:3.12.0' 25 | implementation 'com.vtence.molecule:molecule:0.15.0' 26 | implementation 'io.thorntail:undertow:2.7.0.Final' 27 | implementation 'com.google.code.gson:gson:2.10' 28 | 29 | implementation 'org.flywaydb:flyway-maven-plugin:9.11.0' 30 | } 31 | 32 | test { 33 | useJUnitPlatform() 34 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter15-plus-flyway-support/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/ProductionRandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import com.wordz.domain.RandomNumbers; 4 | 5 | import java.util.Random; 6 | 7 | class ProductionRandomNumbers implements RandomNumbers { 8 | private final Random random = new Random(); 9 | 10 | @Override 11 | public int next(int upperBoundInclusive) { 12 | int upperBoundExclusive = upperBoundInclusive + 1; 13 | return random.nextInt(upperBoundExclusive); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/WordzApplication.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import com.wordz.adapters.api.WordzEndpoint; 4 | import com.wordz.adapters.db.DatabaseMigration; 5 | import com.wordz.adapters.db.GameRepositoryPostgres; 6 | import com.wordz.adapters.db.WordRepositoryPostgres; 7 | import com.wordz.domain.Wordz; 8 | 9 | public class WordzApplication { 10 | 11 | public static void main(String[] args) { 12 | var config = new WordzConfiguration(args); 13 | new WordzApplication().run(config); 14 | } 15 | 16 | private void run(WordzConfiguration config) { 17 | new DatabaseMigration(config.getDataSource()).execute(); 18 | 19 | var gameRepository = new GameRepositoryPostgres(config.getDataSource()); 20 | var wordRepository = new WordRepositoryPostgres(config.getDataSource()); 21 | var randomNumbers = new ProductionRandomNumbers(); 22 | 23 | var wordz = new Wordz(gameRepository, wordRepository, randomNumbers); 24 | 25 | var api = new WordzEndpoint(wordz, 26 | config.getEndpointHost(), 27 | config.getEndpointPort()); 28 | 29 | waitUntilTerminated(); 30 | } 31 | 32 | private void waitUntilTerminated() { 33 | try { 34 | while (true) { 35 | Thread.sleep(10000); 36 | } 37 | } catch (InterruptedException e) { 38 | return; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/WordzConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import org.postgresql.ds.PGSimpleDataSource; 4 | 5 | import javax.sql.DataSource; 6 | 7 | class WordzConfiguration { 8 | public static final int NUMBER_OF_COMMAND_LINE_OPTIONS = 7; 9 | private final PGSimpleDataSource dataSource; 10 | private int endpointPort = 8080; 11 | private String endpointHost = "localhost"; 12 | private String databaseName = "wordzdb"; 13 | private String databaseSchemaName = "public"; 14 | private String databaseUser = "ciuser"; 15 | private String databaseUserPassword = "cipassword"; 16 | private String databaseHost = "localhost"; 17 | 18 | public WordzConfiguration(String[] args) { 19 | extractValuesFromCommandLine(args); 20 | dataSource = createDataSource(); 21 | } 22 | 23 | private PGSimpleDataSource createDataSource() { 24 | var dataSource = new PGSimpleDataSource(); 25 | dataSource.setServerNames(new String[]{databaseHost}); 26 | dataSource.setDatabaseName(databaseName); 27 | dataSource.setCurrentSchema(databaseSchemaName); 28 | dataSource.setUser(databaseUser); 29 | dataSource.setPassword(databaseUserPassword); 30 | return dataSource; 31 | } 32 | 33 | private void extractValuesFromCommandLine(String[] args) { 34 | if (!(args.length == NUMBER_OF_COMMAND_LINE_OPTIONS)) { 35 | return; 36 | } 37 | 38 | endpointHost = args[0]; 39 | endpointPort = Integer.parseInt(args[1]); 40 | databaseName = args[2]; 41 | databaseHost = args[3]; 42 | databaseSchemaName = args[4]; 43 | databaseUser = args[5]; 44 | databaseUserPassword = args[6]; 45 | } 46 | 47 | public DataSource getDataSource() { 48 | return dataSource; 49 | } 50 | 51 | public int getEndpointPort() { 52 | return endpointPort; 53 | } 54 | 55 | public String getEndpointHost() { 56 | return endpointHost; 57 | } 58 | 59 | public String getDatabaseName() { 60 | return databaseName; 61 | } 62 | 63 | public String getDatabaseSchemaName() { 64 | return databaseSchemaName; 65 | } 66 | 67 | public String getDatabaseUser() { 68 | return databaseUser; 69 | } 70 | 71 | public String getDatabaseUserPassword() { 72 | return databaseUserPassword; 73 | } 74 | 75 | public String getDatabaseHost() { 76 | return databaseHost; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/api/GuessHttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | public record GuessHttpResponse(String scores, 4 | boolean isGameOver) { 5 | } 6 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/api/GuessHttpResponseMapper.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.GuessResult; 4 | import com.wordz.domain.Letter; 5 | import com.wordz.domain.Score; 6 | 7 | public class GuessHttpResponseMapper { 8 | GuessHttpResponse from(GuessResult result) { 9 | Score score = result.score(); 10 | 11 | return new GuessHttpResponse(toEndpointResultsFormat(score), 12 | result.isGameOver()); 13 | } 14 | 15 | private String toEndpointResultsFormat(Score score) { 16 | var results = new StringBuilder(); 17 | score.letters().forEach(letter -> results.append(convert(letter))); 18 | return results.toString(); 19 | } 20 | 21 | private char convert(Letter letter) { 22 | return switch(letter) { 23 | case CORRECT -> 'C'; 24 | case PART_CORRECT -> 'P'; 25 | case INCORRECT -> 'X'; 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/api/GuessRequest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.Player; 4 | 5 | public record GuessRequest(Player player, String guess) { 6 | } 7 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/api/WordzEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonSyntaxException; 5 | import com.vtence.molecule.Request; 6 | import com.vtence.molecule.Response; 7 | import com.vtence.molecule.WebServer; 8 | import com.vtence.molecule.http.HttpStatus; 9 | import com.vtence.molecule.routing.Routes; 10 | import com.wordz.domain.GuessResult; 11 | import com.wordz.domain.Player; 12 | import com.wordz.domain.Wordz; 13 | 14 | import java.io.IOException; 15 | 16 | import static com.vtence.molecule.http.HttpStatus.*; 17 | 18 | public class WordzEndpoint { 19 | private final Wordz wordz; 20 | 21 | public WordzEndpoint(Wordz wordz, String host, int port) { 22 | this.wordz = wordz; 23 | var server = WebServer.create(host, port); 24 | 25 | try { 26 | server.route(new Routes() {{ 27 | post("/start").to(request -> startGame(request)); 28 | post("/guess").to(request -> guessWord(request)); 29 | }}); 30 | } catch (IOException e) { 31 | throw new IllegalStateException("Wordz HTTP endpoint could not start", e); 32 | } 33 | } 34 | 35 | private Response startGame(Request request) { 36 | try { 37 | Player player = extractPlayer(request); 38 | 39 | boolean isSuccessful = wordz.newGame(player); 40 | HttpStatus status 41 | = isSuccessful? NO_CONTENT : CONFLICT; 42 | 43 | return Response.of(status).done(); 44 | } catch (IOException | JsonSyntaxException e) { 45 | return Response.of(BAD_REQUEST).done(); 46 | } 47 | } 48 | 49 | private Response guessWord(Request request) { 50 | try { 51 | GuessRequest gr = extractGuessRequest(request); 52 | GuessResult result = wordz.assess(gr.player(), gr.guess()); 53 | 54 | if (result.isError()) { 55 | return Response.of(INTERNAL_SERVER_ERROR).done(); 56 | } 57 | 58 | return Response.ok() 59 | .body(createGuessHttpResponse(result)) 60 | .done(); 61 | } catch (IOException e) { 62 | return Response.of(INTERNAL_SERVER_ERROR).done(); 63 | } 64 | } 65 | 66 | private GuessRequest extractGuessRequest(Request request) throws IOException { 67 | return new Gson().fromJson(request.body(), GuessRequest.class); 68 | } 69 | 70 | private String createGuessHttpResponse(GuessResult result) { 71 | GuessHttpResponse httpResponse 72 | = new GuessHttpResponseMapper().from(result); 73 | return new Gson().toJson(httpResponse); 74 | } 75 | 76 | 77 | private Player extractPlayer(Request request) throws IOException { 78 | return new Gson().fromJson(request.body(), Player.class); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/db/DatabaseMigration.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import org.flywaydb.core.Flyway; 4 | 5 | import javax.sql.DataSource; 6 | 7 | public class DatabaseMigration { 8 | private static final String LOCATION_FLYWAY_CONFIG 9 | = "classpath:db/postgres"; 10 | 11 | private final DataSource dataSource; 12 | 13 | public DatabaseMigration(DataSource dataSource) { 14 | this.dataSource = dataSource; 15 | } 16 | 17 | public void execute() { 18 | Flyway flyway = 19 | Flyway.configure() 20 | .locations(LOCATION_FLYWAY_CONFIG) 21 | .dataSource(dataSource) 22 | .baselineOnMigrate(true) 23 | .load(); 24 | 25 | flyway.migrate(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/adapters/db/WordRepositoryPostgres.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.wordz.domain.WordRepository; 4 | import org.jdbi.v3.core.Jdbi; 5 | 6 | import javax.sql.DataSource; 7 | 8 | public class WordRepositoryPostgres implements WordRepository { 9 | private static final String SQL_FETCH_WORD_BY_NUMBER 10 | = "select word from word where word_number=:wordNumber"; 11 | private static final String SQL_RETURN_HIGHEST_WORD_NUMBER 12 | = "select max(word_number) from word"; 13 | private final Jdbi jdbi; 14 | 15 | public WordRepositoryPostgres(DataSource dataSource) { 16 | jdbi = Jdbi.create(dataSource); 17 | } 18 | 19 | @Override 20 | public String fetchWordByNumber(int wordNumber) { 21 | String word = jdbi.withHandle(handle -> { 22 | var query = handle.createQuery(SQL_FETCH_WORD_BY_NUMBER); 23 | query.bind("wordNumber", wordNumber); 24 | 25 | return query 26 | .mapTo(String.class) 27 | .one(); 28 | }); 29 | 30 | return word ; 31 | } 32 | 33 | @Override 34 | public int highestWordNumber() { 35 | return jdbi.withHandle(handle-> 36 | handle.createQuery(SQL_RETURN_HIGHEST_WORD_NUMBER) 37 | .mapTo(Integer.class) 38 | .one()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Game.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Game { 4 | private static final int MAXIMUM_NUMBER_ALLOWED_GUESSES = 5; 5 | private final Player player; 6 | private final String targetWord; 7 | private int attemptNumber; 8 | private boolean isGameOver; 9 | 10 | public Game(Player player, String targetWord, int attemptNumber, boolean isGameOver) { 11 | this.player = player; 12 | this.targetWord = targetWord; 13 | this.attemptNumber = attemptNumber; 14 | this.isGameOver = isGameOver; 15 | } 16 | 17 | static Game create(Player player, String correctWord) { 18 | return new Game(player, correctWord, 0, false); 19 | } 20 | 21 | static Game create(Player player, String correctWord, int attemptNumber) { 22 | return new Game(player, correctWord, attemptNumber, false); 23 | } 24 | 25 | public Player getPlayer() { 26 | return player; 27 | } 28 | 29 | public int getAttemptNumber() { 30 | return attemptNumber; 31 | } 32 | 33 | public String getWord() { 34 | return targetWord; 35 | } 36 | 37 | public Score attempt(String guess) { 38 | trackNumberOfAttempts(); 39 | 40 | var word = new Word(targetWord); 41 | Score score = word.guess(guess); 42 | 43 | if (score.allCorrect()) { 44 | end(); 45 | } 46 | 47 | return score; 48 | } 49 | 50 | private void trackNumberOfAttempts() { 51 | attemptNumber++; 52 | 53 | if (attemptNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES) { 54 | end(); 55 | } 56 | } 57 | 58 | public boolean isGameOver() { 59 | return isGameOver; 60 | } 61 | 62 | void end() { 63 | isGameOver = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/GameRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface GameRepository { 6 | void create(Game game); 7 | 8 | Optional fetchForPlayer(Player player); 9 | 10 | void update(Game game); 11 | } 12 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/GuessResult.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public record GuessResult( 4 | Score score, 5 | boolean isGameOver, 6 | boolean isError 7 | ) { 8 | static final GuessResult ERROR = new GuessResult(null, true, true); 9 | 10 | static GuessResult create(Score score, boolean isGameOver) { 11 | return new GuessResult(score, isGameOver, false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Player.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 4 | import static org.apache.commons.lang3.builder.HashCodeBuilder.*; 5 | 6 | public class Player { 7 | private final String name; 8 | 9 | public Player(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object other) { 19 | return reflectionEquals(this, other); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return reflectionHashCode(this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.util.Collections.unmodifiableList; 7 | 8 | public class Score { 9 | private final String correct; 10 | private final List results = new ArrayList<>(); 11 | private int position; 12 | 13 | public Score(String correct) { 14 | this.correct = correct; 15 | } 16 | 17 | public Letter letter(int position) { 18 | return results.get(position); 19 | } 20 | 21 | public void assess(String attempt) { 22 | for (char current: attempt.toCharArray()) { 23 | results.add( scoreFor(current) ); 24 | position++; 25 | } 26 | } 27 | 28 | public boolean allCorrect() { 29 | var totalCorrect = results.stream() 30 | .filter(letter -> letter == Letter.CORRECT) 31 | .count(); 32 | 33 | return totalCorrect == results.size(); 34 | } 35 | 36 | public List letters() { 37 | return unmodifiableList(results); 38 | } 39 | 40 | private Letter scoreFor(char current) { 41 | if (isCorrectLetter(current)) { 42 | return Letter.CORRECT; 43 | } 44 | 45 | if (occursInWord(current)) { 46 | return Letter.PART_CORRECT; 47 | } 48 | 49 | return Letter.INCORRECT; 50 | } 51 | 52 | private boolean occursInWord(char current) { 53 | return correct.contains(String.valueOf(current)); 54 | } 55 | 56 | private boolean isCorrectLetter(char currentLetter) { 57 | return correct.charAt(position) == currentLetter; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/java/com/wordz/domain/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public class Wordz { 6 | private final GameRepository gameRepository; 7 | private final WordSelection selection ; 8 | 9 | public Wordz(GameRepository repository, WordRepository wordRepository, RandomNumbers randomNumbers) { 10 | this.gameRepository = repository; 11 | this.selection = new WordSelection(wordRepository, randomNumbers); 12 | } 13 | 14 | public boolean newGame(Player player) { 15 | Optional currentGame = gameRepository.fetchForPlayer(player); 16 | 17 | if (isGameInProgress(currentGame)) { 18 | return false ; 19 | } 20 | 21 | var word = selection.chooseRandomWord(); 22 | Game game = new Game(player, word, 0, false); 23 | gameRepository.create(game); 24 | return true; 25 | } 26 | 27 | public GuessResult assess(Player player, String guess) { 28 | Optional currentGame = gameRepository.fetchForPlayer(player); 29 | 30 | if (!isGameInProgress(currentGame)) { 31 | return GuessResult.ERROR; 32 | } 33 | 34 | return calculateScore(currentGame.get(), guess); 35 | } 36 | 37 | private GuessResult calculateScore(Game game, String guess) { 38 | Score score = game.attempt( guess ); 39 | 40 | gameRepository.update(game); 41 | return GuessResult.create(score, game.isGameOver()); 42 | } 43 | 44 | private boolean isGameInProgress(Optional currentGame) { 45 | if (currentGame.isEmpty()) { 46 | return false ; 47 | } 48 | 49 | return !currentGame.get().isGameOver(); 50 | } 51 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/main/resources/db/postgres/V2__initial_schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists word ( 2 | word_number int primary key, 3 | word char(5) 4 | ); 5 | 6 | create table if not exists game ( 7 | player_name character varying NOT NULL, 8 | word character(5), 9 | attempt_number integer DEFAULT 0, 10 | is_game_over boolean DEFAULT false 11 | ); 12 | 13 | insert into word (word_number, word) values 14 | (1, 'ARISE'), 15 | (2, 'SHINE'), 16 | (3, 'LIGHT'), 17 | (4, 'AGREE'), 18 | (5, 'GEARS'), 19 | (6, 'STAYS'), 20 | (7, 'SOLID'), 21 | (8, 'TESTS'), 22 | (9, 'THINK'), 23 | (10, 'THROW') on conflict do nothing; -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/adapters/api/GuessHttpResponseMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.GuessResult; 4 | import com.wordz.domain.Score; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | 12 | class GuessHttpResponseMapperTest { 13 | private GuessHttpResponse actual; 14 | 15 | @BeforeEach 16 | void setup() { 17 | // Correct, Part correct, Incorrect, Incorrect, Incorrect 18 | Score score = new Score("ABCZZ"); 19 | score.assess("ACBXX"); 20 | 21 | var guessResult = new GuessResult(score, true, true); 22 | 23 | actual = new GuessHttpResponseMapper().from(guessResult); 24 | } 25 | 26 | @Test 27 | void mapsCorrectLetter() { 28 | assertThat(actual.scores().charAt(0)).isEqualTo('C'); 29 | } 30 | 31 | @Test 32 | void mapsPartCorrectLetter() { 33 | assertThat(actual.scores().charAt(1)).isEqualTo('P'); 34 | } 35 | 36 | @Test 37 | void mapsIncorrectLetter() { 38 | assertThat(actual.scores().charAt(4)).isEqualTo('X'); 39 | } 40 | 41 | @Test 42 | void mapsGameOver() { 43 | assertThat(actual.isGameOver()).isTrue(); 44 | } 45 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/adapters/db/PostgresTestDataSource.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import org.postgresql.ds.PGSimpleDataSource; 4 | 5 | public class PostgresTestDataSource extends PGSimpleDataSource { 6 | PostgresTestDataSource () { 7 | setServerNames(new String[]{"localhost"}); 8 | setDatabaseName("wordzdb"); 9 | setCurrentSchema("public"); 10 | setUser("ciuser"); 11 | setPassword("cipassword"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/adapters/db/WordRepositoryPostgresTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.github.database.rider.core.api.configuration.DBUnit; 4 | import com.github.database.rider.core.api.configuration.Orthography; 5 | import com.github.database.rider.core.api.connection.ConnectionHolder; 6 | import com.github.database.rider.core.api.dataset.DataSet; 7 | import com.github.database.rider.junit5.api.DBRider; 8 | import com.wordz.domain.WordRepository; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import javax.sql.DataSource; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @DBRider 17 | @DBUnit(caseSensitiveTableNames = true, 18 | caseInsensitiveStrategy= Orthography.LOWERCASE) 19 | public class WordRepositoryPostgresTest { 20 | 21 | private DataSource dataSource; 22 | 23 | @SuppressWarnings("unused") // Used by DBRider framework 24 | private final ConnectionHolder connectionHolder = () -> dataSource.getConnection(); 25 | 26 | /** 27 | * As this method only sets up a connection to the database and does not 28 | * create any stored data, there is a good argument for using the @BeforeAll 29 | * annotation and doing this once for all tests in this class 30 | */ 31 | @BeforeEach 32 | void setupConnection() { 33 | this.dataSource = new PostgresTestDataSource(); 34 | } 35 | 36 | @Test 37 | @DataSet("adapters/data/wordTable.json") 38 | public void fetchesWord() { 39 | WordRepository repository = new WordRepositoryPostgres(dataSource); 40 | 41 | String actual = repository.fetchWordByNumber(27); 42 | 43 | assertThat(actual).isEqualTo("ARISE"); 44 | } 45 | 46 | @Test 47 | @DataSet("adapters/data/threeWords.json") 48 | public void returnsHighestWordNumber() { 49 | WordRepository repository = new WordRepositoryPostgres(dataSource); 50 | 51 | int actual = repository.highestWordNumber(); 52 | 53 | assertThat(actual).isEqualTo(3); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/domain/GuessTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.Optional; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.Mockito.*; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class GuessTest { 17 | private static final Player PLAYER = new Player("player1"); 18 | private static final String CORRECT_WORD = "ARISE"; 19 | private static final String WRONG_WORD = "RXXXX"; 20 | @Mock 21 | private GameRepository gameRepository; 22 | @InjectMocks 23 | private Wordz wordz; 24 | 25 | @Test 26 | void returnsScoreForGuess() { 27 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 28 | 29 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 30 | 31 | Letter firstLetter = result.score().letter(0); 32 | assertThat(firstLetter).isEqualTo(Letter.PART_CORRECT); 33 | } 34 | 35 | @Test 36 | void updatesAttemptNumber() { 37 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 38 | 39 | wordz.assess(PLAYER, WRONG_WORD); 40 | 41 | var game = getUpdatedGameInRepository(); 42 | assertThat(game.getAttemptNumber()).isEqualTo(1); 43 | } 44 | 45 | @Test 46 | void gameOverOnTooManyIncorrectGuesses(){ 47 | int maximumGuesses = 5; 48 | givenGameInRepository( 49 | Game.create(PLAYER, CORRECT_WORD, 50 | maximumGuesses-1)); 51 | 52 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 53 | 54 | assertThat(result.isGameOver()).isTrue(); 55 | } 56 | 57 | @Test 58 | void rejectsGuessAfterGameOver(){ 59 | var game = Game.create(PLAYER, CORRECT_WORD); 60 | game.end(); 61 | givenGameInRepository( game ); 62 | 63 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 64 | 65 | assertThat(result.isError()).isTrue(); 66 | } 67 | 68 | @Test 69 | void recordsGameOverOnCorrectGuess(){ 70 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 71 | 72 | wordz.assess(PLAYER, CORRECT_WORD); 73 | 74 | Game game = getUpdatedGameInRepository(); 75 | assertThat(game.isGameOver()).isTrue(); 76 | } 77 | 78 | private Game getUpdatedGameInRepository() { 79 | ArgumentCaptor argument = ArgumentCaptor.forClass(Game.class); 80 | verify(gameRepository).update(argument.capture()); 81 | return argument.getValue(); 82 | } 83 | 84 | private void givenGameInRepository(Game game) { 85 | when(gameRepository.fetchForPlayer(eq(PLAYER))) 86 | .thenReturn(Optional.of(game)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/domain/NewGameTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.*; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.mockito.ArgumentMatchers.anyInt; 13 | import static org.mockito.Mockito.*; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class NewGameTest { 19 | private static final Player PLAYER = new Player("player1"); 20 | private static final String CORRECT_WORD = "ARISE"; 21 | 22 | @Mock 23 | private GameRepository gameRepository; 24 | @Mock 25 | private WordRepository wordRepository ; 26 | @Mock 27 | private RandomNumbers randomNumbers ; 28 | @InjectMocks 29 | private Wordz wordz; 30 | 31 | @Test 32 | void startsNewGame() { 33 | givenWordToSelect(CORRECT_WORD); 34 | 35 | wordz.newGame(PLAYER); 36 | 37 | Game game = getGameInRepository(); 38 | assertThat(game.getWord()).isEqualTo(CORRECT_WORD); 39 | assertThat(game.getAttemptNumber()).isZero(); 40 | assertThat(game.getPlayer()).isSameAs(PLAYER); 41 | } 42 | 43 | @Test 44 | void cannotRestartGameInProgress() { 45 | when(gameRepository 46 | .fetchForPlayer(eq(PLAYER))) 47 | .thenReturn(Optional.of( 48 | Game.create(PLAYER, CORRECT_WORD))); 49 | var success = wordz.newGame(PLAYER); 50 | 51 | assertThat(success).isFalse(); 52 | } 53 | 54 | private void givenWordToSelect(String wordToSelect) { 55 | int wordNumber = 2; 56 | 57 | when(randomNumbers.next(anyInt())) 58 | .thenReturn(wordNumber); 59 | 60 | when(wordRepository 61 | .fetchWordByNumber(wordNumber)) 62 | .thenReturn(wordToSelect); 63 | } 64 | 65 | private Game getGameInRepository() { 66 | var gameArgument = ArgumentCaptor.forClass(Game.class); 67 | verify(gameRepository) 68 | .create(gameArgument.capture()); 69 | var game = gameArgument.getValue(); 70 | return game; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/domain/PlayerTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class PlayerTest { 8 | 9 | @Test 10 | void twoAreEqual() { 11 | Player a = new Player("same-name-means-equal"); 12 | Player b = new Player("same-name-means-equal"); 13 | 14 | assertThat(a).isEqualTo(b); 15 | } 16 | 17 | @Test 18 | void twoAreNotEqual() { 19 | Player a = new Player("same-name-means-equal"); 20 | Player b = new Player("different-name-means-not-equal"); 21 | 22 | assertThat(a).isNotEqualTo(b); 23 | } 24 | 25 | @Test 26 | void equalHaveSameHashcode() { 27 | Player a = new Player("same-name-means-equal"); 28 | Player b = new Player("same-name-means-equal"); 29 | 30 | assertThat(a.hashCode()).isEqualTo(b.hashCode()); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | @Test 41 | void reportsAllCorrect() { 42 | var word = new Word("ARISE"); 43 | var score = word.guess("ARISE"); 44 | assertThat(score.allCorrect()).isTrue(); 45 | } 46 | 47 | @Test 48 | void reportsNotAllCorrect() { 49 | var word = new Word("ARISE"); 50 | var score = word.guess("ARI*E"); 51 | assertThat(score.allCorrect()).isFalse(); 52 | } 53 | 54 | @Test 55 | void accessesLetters() { 56 | var word = new Word("ARISE"); 57 | var score = word.guess("ARI*E"); 58 | assertThat(score.letters()).hasSize(5); 59 | } 60 | 61 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 62 | for (int position = 0; position < expectedScores.length; position++) { 63 | Letter expected = expectedScores[position]; 64 | assertThat(score.letter(position)) 65 | .isEqualTo(expected); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/createGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 0, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/emptyGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/initialSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { "word_number": 1, "word": "ARISE" }, 4 | { "word_number": 2, "word": "SHINE" }, 5 | { "word_number": 3, "word": "LIGHT" }, 6 | { "word_number": 4, "word": "AGREE" }, 7 | { "word_number": 5, "word": "GEARS" }, 8 | { "word_number": 6, "word": "STAYS" }, 9 | { "word_number": 7, "word": "SOLID" }, 10 | { "word_number": 8, "word": "TESTS" }, 11 | { "word_number": 9, "word": "THINK" }, 12 | { "word_number": 10, "word": "THROW" } 13 | ], 14 | "game": [ 15 | ] 16 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/threeWords.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 1, 5 | "word": "ARISE" 6 | }, 7 | { 8 | "word_number": 2, 9 | "word": "SHINE" 10 | }, 11 | { 12 | "word_number": 3, 13 | "word": "SLEEP" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/updatedGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 1, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter15-plus-flyway-support/src/test/resources/adapters/data/wordTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 27, 5 | "word": "ARISE" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /chapter15/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/gitignore 2 | # From: https://gist.github.com/edesdan/6bb43343740bcd54ef0f56a384a2f66f 3 | 4 | ###################### 5 | ###### Mac OS X ###### 6 | ###################### 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | 20 | ###################### 21 | ###### Java ###### 22 | ###################### 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.ear 34 | *.zip 35 | *.tar.gz 36 | *.rar 37 | 38 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 39 | hs_err_pid* 40 | 41 | ###################### 42 | ###### Gradle ###### 43 | ###################### 44 | 45 | .gradle 46 | build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | ###################### 61 | ###### Intellij ###### 62 | ###################### 63 | 64 | # Created by https://www.gitignore.io/api/intellij 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 66 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 67 | 68 | # User-specific stuff: 69 | .idea/ 70 | .idea/**/workspace.xml 71 | .idea/**/tasks.xml 72 | .idea/dictionaries 73 | 74 | # Sensitive or high-churn files: 75 | .idea/**/dataSources/ 76 | .idea/**/dataSources.ids 77 | .idea/**/dataSources.xml 78 | .idea/**/dataSources.local.xml 79 | .idea/**/sqlDataSources.xml 80 | .idea/**/dynamic.xml 81 | .idea/**/uiDesigner.xml 82 | 83 | # Gradle: 84 | .idea/**/gradle.xml 85 | .idea/**/libraries 86 | 87 | ## File-based project format: 88 | *.iws 89 | 90 | ## Plugin-specific files: 91 | 92 | # IntelliJ 93 | /out/ 94 | 95 | # mpeltonen/sbt-idea plugin 96 | .idea_modules/ 97 | -------------------------------------------------------------------------------- /chapter15/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.wordz' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 15 | testImplementation 'org.assertj:assertj-core:3.22.0' 16 | testImplementation 'org.hamcrest:hamcrest:2.2' 17 | testImplementation 'org.mockito:mockito-core:4.8.0' 18 | testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' 19 | testImplementation 'com.github.database-rider:rider-core:1.35.0' 20 | testImplementation 'com.github.database-rider:rider-junit5:1.35.0' 21 | 22 | implementation 'org.postgresql:postgresql:42.5.0' 23 | implementation 'org.jdbi:jdbi3-core:3.34.0' 24 | implementation 'org.apache.commons:commons-lang3:3.12.0' 25 | implementation 'com.vtence.molecule:molecule:0.15.0' 26 | implementation 'io.thorntail:undertow:2.7.0.Final' 27 | implementation 'com.google.code.gson:gson:2.10' 28 | 29 | } 30 | 31 | test { 32 | useJUnitPlatform() 33 | } -------------------------------------------------------------------------------- /chapter15/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Test-Driven-Development-with-Java/c558b327a49ac05ccde0e67616858f98f47dec06/chapter15/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter15/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chapter15/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wordz' 2 | 3 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/ProductionRandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import com.wordz.domain.RandomNumbers; 4 | 5 | import java.util.Random; 6 | 7 | class ProductionRandomNumbers implements RandomNumbers { 8 | private final Random random = new Random(); 9 | 10 | @Override 11 | public int next(int upperBoundInclusive) { 12 | int upperBoundExclusive = upperBoundInclusive + 1; 13 | return random.nextInt(upperBoundExclusive); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/WordzApplication.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import com.wordz.adapters.api.WordzEndpoint; 4 | import com.wordz.adapters.db.GameRepositoryPostgres; 5 | import com.wordz.adapters.db.WordRepositoryPostgres; 6 | import com.wordz.domain.Wordz; 7 | 8 | public class WordzApplication { 9 | public static void main(String[] args) { 10 | var config = new WordzConfiguration(args); 11 | new WordzApplication().run(config); 12 | } 13 | 14 | private void run(WordzConfiguration config) { 15 | var gameRepository = new GameRepositoryPostgres(config.getDataSource()); 16 | var wordRepository = new WordRepositoryPostgres(config.getDataSource()); 17 | var randomNumbers = new ProductionRandomNumbers(); 18 | 19 | var wordz = new Wordz(gameRepository, wordRepository, randomNumbers); 20 | 21 | var api = new WordzEndpoint(wordz, 22 | config.getEndpointHost(), 23 | config.getEndpointPort()); 24 | 25 | waitUntilTerminated(); 26 | } 27 | 28 | private void waitUntilTerminated() { 29 | try { 30 | while (true) { 31 | Thread.sleep(10000); 32 | } 33 | } catch (InterruptedException e) { 34 | return; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/WordzConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.wordz; 2 | 3 | import org.postgresql.ds.PGSimpleDataSource; 4 | 5 | import javax.sql.DataSource; 6 | 7 | class WordzConfiguration { 8 | public static final int NUMBER_OF_COMMAND_LINE_OPTIONS = 7; 9 | private final PGSimpleDataSource dataSource; 10 | private int endpointPort = 8080; 11 | private String endpointHost = "localhost"; 12 | private String databaseName = "wordzdb"; 13 | private String databaseSchemaName = "public"; 14 | private String databaseUser = "ciuser"; 15 | private String databaseUserPassword = "cipassword"; 16 | private String databaseHost = "localhost"; 17 | 18 | public WordzConfiguration(String[] args) { 19 | extractValuesFromCommandLine(args); 20 | dataSource = createDataSource(); 21 | } 22 | 23 | private PGSimpleDataSource createDataSource() { 24 | var dataSource = new PGSimpleDataSource(); 25 | dataSource.setServerNames(new String[]{databaseHost}); 26 | dataSource.setDatabaseName(databaseName); 27 | dataSource.setCurrentSchema(databaseSchemaName); 28 | dataSource.setUser(databaseUser); 29 | dataSource.setPassword(databaseUserPassword); 30 | return dataSource; 31 | } 32 | 33 | private void extractValuesFromCommandLine(String[] args) { 34 | if (!(args.length == NUMBER_OF_COMMAND_LINE_OPTIONS)) { 35 | return; 36 | } 37 | 38 | endpointHost = args[0]; 39 | endpointPort = Integer.parseInt(args[1]); 40 | databaseName = args[2]; 41 | databaseHost = args[3]; 42 | databaseSchemaName = args[4]; 43 | databaseUser = args[5]; 44 | databaseUserPassword = args[6]; 45 | } 46 | 47 | public DataSource getDataSource() { 48 | return dataSource; 49 | } 50 | 51 | public int getEndpointPort() { 52 | return endpointPort; 53 | } 54 | 55 | public String getEndpointHost() { 56 | return endpointHost; 57 | } 58 | 59 | public String getDatabaseName() { 60 | return databaseName; 61 | } 62 | 63 | public String getDatabaseSchemaName() { 64 | return databaseSchemaName; 65 | } 66 | 67 | public String getDatabaseUser() { 68 | return databaseUser; 69 | } 70 | 71 | public String getDatabaseUserPassword() { 72 | return databaseUserPassword; 73 | } 74 | 75 | public String getDatabaseHost() { 76 | return databaseHost; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/adapters/api/GuessHttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | public record GuessHttpResponse(String scores, 4 | boolean isGameOver) { 5 | } 6 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/adapters/api/GuessHttpResponseMapper.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.GuessResult; 4 | import com.wordz.domain.Letter; 5 | import com.wordz.domain.Score; 6 | 7 | public class GuessHttpResponseMapper { 8 | GuessHttpResponse from(GuessResult result) { 9 | Score score = result.score(); 10 | 11 | return new GuessHttpResponse(toEndpointResultsFormat(score), 12 | result.isGameOver()); 13 | } 14 | 15 | private String toEndpointResultsFormat(Score score) { 16 | var results = new StringBuilder(); 17 | score.letters().forEach(letter -> results.append(convert(letter))); 18 | return results.toString(); 19 | } 20 | 21 | private char convert(Letter letter) { 22 | return switch(letter) { 23 | case CORRECT -> 'C'; 24 | case PART_CORRECT -> 'P'; 25 | case INCORRECT -> 'X'; 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/adapters/api/GuessRequest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.Player; 4 | 5 | public record GuessRequest(Player player, String guess) { 6 | } 7 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/adapters/api/WordzEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonSyntaxException; 5 | import com.vtence.molecule.Request; 6 | import com.vtence.molecule.Response; 7 | import com.vtence.molecule.WebServer; 8 | import com.vtence.molecule.http.HttpStatus; 9 | import com.vtence.molecule.routing.Routes; 10 | import com.wordz.domain.GuessResult; 11 | import com.wordz.domain.Player; 12 | import com.wordz.domain.Wordz; 13 | 14 | import java.io.IOException; 15 | 16 | import static com.vtence.molecule.http.HttpStatus.*; 17 | 18 | public class WordzEndpoint { 19 | private final Wordz wordz; 20 | 21 | public WordzEndpoint(Wordz wordz, String host, int port) { 22 | this.wordz = wordz; 23 | var server = WebServer.create(host, port); 24 | 25 | try { 26 | server.route(new Routes() {{ 27 | post("/start").to(request -> startGame(request)); 28 | post("/guess").to(request -> guessWord(request)); 29 | }}); 30 | } catch (IOException e) { 31 | throw new IllegalStateException("Wordz HTTP endpoint could not start", e); 32 | } 33 | } 34 | 35 | private Response startGame(Request request) { 36 | try { 37 | Player player = extractPlayer(request); 38 | 39 | boolean isSuccessful = wordz.newGame(player); 40 | HttpStatus status 41 | = isSuccessful? NO_CONTENT : CONFLICT; 42 | 43 | return Response.of(status).done(); 44 | } catch (IOException | JsonSyntaxException e) { 45 | return Response.of(BAD_REQUEST).done(); 46 | } 47 | } 48 | 49 | private Response guessWord(Request request) { 50 | try { 51 | GuessRequest gr = extractGuessRequest(request); 52 | GuessResult result = wordz.assess(gr.player(), gr.guess()); 53 | 54 | if (result.isError()) { 55 | return Response.of(INTERNAL_SERVER_ERROR).done(); 56 | } 57 | 58 | return Response.ok() 59 | .body(createGuessHttpResponse(result)) 60 | .done(); 61 | } catch (IOException e) { 62 | return Response.of(INTERNAL_SERVER_ERROR).done(); 63 | } 64 | } 65 | 66 | private GuessRequest extractGuessRequest(Request request) throws IOException { 67 | return new Gson().fromJson(request.body(), GuessRequest.class); 68 | } 69 | 70 | private String createGuessHttpResponse(GuessResult result) { 71 | GuessHttpResponse httpResponse 72 | = new GuessHttpResponseMapper().from(result); 73 | return new Gson().toJson(httpResponse); 74 | } 75 | 76 | 77 | private Player extractPlayer(Request request) throws IOException { 78 | return new Gson().fromJson(request.body(), Player.class); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/adapters/db/WordRepositoryPostgres.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.wordz.domain.WordRepository; 4 | import org.jdbi.v3.core.Jdbi; 5 | 6 | import javax.sql.DataSource; 7 | 8 | public class WordRepositoryPostgres implements WordRepository { 9 | private static final String SQL_FETCH_WORD_BY_NUMBER 10 | = "select word from word where word_number=:wordNumber"; 11 | private static final String SQL_RETURN_HIGHEST_WORD_NUMBER 12 | = "select max(word_number) from word"; 13 | private final Jdbi jdbi; 14 | 15 | public WordRepositoryPostgres(DataSource dataSource) { 16 | jdbi = Jdbi.create(dataSource); 17 | } 18 | 19 | @Override 20 | public String fetchWordByNumber(int wordNumber) { 21 | String word = jdbi.withHandle(handle -> { 22 | var query = handle.createQuery(SQL_FETCH_WORD_BY_NUMBER); 23 | query.bind("wordNumber", wordNumber); 24 | 25 | return query 26 | .mapTo(String.class) 27 | .one(); 28 | }); 29 | 30 | return word ; 31 | } 32 | 33 | @Override 34 | public int highestWordNumber() { 35 | return jdbi.withHandle(handle-> 36 | handle.createQuery(SQL_RETURN_HIGHEST_WORD_NUMBER) 37 | .mapTo(Integer.class) 38 | .one()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Game.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Game { 4 | private static final int MAXIMUM_NUMBER_ALLOWED_GUESSES = 5; 5 | private final Player player; 6 | private final String targetWord; 7 | private int attemptNumber; 8 | private boolean isGameOver; 9 | 10 | public Game(Player player, String targetWord, int attemptNumber, boolean isGameOver) { 11 | this.player = player; 12 | this.targetWord = targetWord; 13 | this.attemptNumber = attemptNumber; 14 | this.isGameOver = isGameOver; 15 | } 16 | 17 | static Game create(Player player, String correctWord) { 18 | return new Game(player, correctWord, 0, false); 19 | } 20 | 21 | static Game create(Player player, String correctWord, int attemptNumber) { 22 | return new Game(player, correctWord, attemptNumber, false); 23 | } 24 | 25 | public Player getPlayer() { 26 | return player; 27 | } 28 | 29 | public int getAttemptNumber() { 30 | return attemptNumber; 31 | } 32 | 33 | public String getWord() { 34 | return targetWord; 35 | } 36 | 37 | public Score attempt(String guess) { 38 | trackNumberOfAttempts(); 39 | 40 | var word = new Word(targetWord); 41 | Score score = word.guess(guess); 42 | 43 | if (score.allCorrect()) { 44 | end(); 45 | } 46 | 47 | return score; 48 | } 49 | 50 | private void trackNumberOfAttempts() { 51 | attemptNumber++; 52 | 53 | if (attemptNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES) { 54 | end(); 55 | } 56 | } 57 | 58 | public boolean isGameOver() { 59 | return isGameOver; 60 | } 61 | 62 | void end() { 63 | isGameOver = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/GameRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface GameRepository { 6 | void create(Game game); 7 | 8 | Optional fetchForPlayer(Player player); 9 | 10 | void update(Game game); 11 | } 12 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/GuessResult.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public record GuessResult( 4 | Score score, 5 | boolean isGameOver, 6 | boolean isError 7 | ) { 8 | static final GuessResult ERROR = new GuessResult(null, true, true); 9 | 10 | static GuessResult create(Score score, boolean isGameOver) { 11 | return new GuessResult(score, isGameOver, false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Letter.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public enum Letter { 4 | CORRECT, PART_CORRECT, INCORRECT 5 | } 6 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Player.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 4 | import static org.apache.commons.lang3.builder.HashCodeBuilder.*; 5 | 6 | public class Player { 7 | private final String name; 8 | 9 | public Player(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object other) { 19 | return reflectionEquals(this, other); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return reflectionHashCode(this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/RandomNumbers.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface RandomNumbers { 4 | int next(int upperBoundInclusive); 5 | } 6 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Score.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.util.Collections.unmodifiableList; 7 | 8 | public class Score { 9 | private final String correct; 10 | private final List results = new ArrayList<>(); 11 | private int position; 12 | 13 | public Score(String correct) { 14 | this.correct = correct; 15 | } 16 | 17 | public Letter letter(int position) { 18 | return results.get(position); 19 | } 20 | 21 | public void assess(String attempt) { 22 | for (char current: attempt.toCharArray()) { 23 | results.add( scoreFor(current) ); 24 | position++; 25 | } 26 | } 27 | 28 | public boolean allCorrect() { 29 | var totalCorrect = results.stream() 30 | .filter(letter -> letter == Letter.CORRECT) 31 | .count(); 32 | 33 | return totalCorrect == results.size(); 34 | } 35 | 36 | public List letters() { 37 | return unmodifiableList(results); 38 | } 39 | 40 | private Letter scoreFor(char current) { 41 | if (isCorrectLetter(current)) { 42 | return Letter.CORRECT; 43 | } 44 | 45 | if (occursInWord(current)) { 46 | return Letter.PART_CORRECT; 47 | } 48 | 49 | return Letter.INCORRECT; 50 | } 51 | 52 | private boolean occursInWord(char current) { 53 | return correct.contains(String.valueOf(current)); 54 | } 55 | 56 | private boolean isCorrectLetter(char currentLetter) { 57 | return correct.charAt(position) == currentLetter; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Word.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class Word { 4 | private final String word; 5 | 6 | public Word(String correctWord) { 7 | this.word = correctWord; 8 | } 9 | 10 | public Score guess(String attempt) { 11 | var score = new Score(word); 12 | 13 | score.assess(attempt); 14 | return score; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/WordRepository.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public interface WordRepository { 4 | String fetchWordByNumber(int number); 5 | 6 | int highestWordNumber(); 7 | } 8 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/WordSelection.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | public class WordSelection { 4 | private final WordRepository repository; 5 | private final RandomNumbers random; 6 | 7 | public WordSelection(WordRepository repository, RandomNumbers random) { 8 | this.repository = repository; 9 | this.random = random; 10 | } 11 | 12 | public String chooseRandomWord() { 13 | int wordNumber = random.next(repository.highestWordNumber()); 14 | 15 | return repository.fetchWordByNumber(wordNumber); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter15/src/main/java/com/wordz/domain/Wordz.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public class Wordz { 6 | private final GameRepository gameRepository; 7 | private final WordSelection selection ; 8 | 9 | public Wordz(GameRepository repository, WordRepository wordRepository, RandomNumbers randomNumbers) { 10 | this.gameRepository = repository; 11 | this.selection = new WordSelection(wordRepository, randomNumbers); 12 | } 13 | 14 | public boolean newGame(Player player) { 15 | Optional currentGame = gameRepository.fetchForPlayer(player); 16 | 17 | if (isGameInProgress(currentGame)) { 18 | return false ; 19 | } 20 | 21 | var word = selection.chooseRandomWord(); 22 | Game game = new Game(player, word, 0, false); 23 | gameRepository.create(game); 24 | return true; 25 | } 26 | 27 | public GuessResult assess(Player player, String guess) { 28 | Optional currentGame = gameRepository.fetchForPlayer(player); 29 | 30 | if (!isGameInProgress(currentGame)) { 31 | return GuessResult.ERROR; 32 | } 33 | 34 | return calculateScore(currentGame.get(), guess); 35 | } 36 | 37 | private GuessResult calculateScore(Game game, String guess) { 38 | Score score = game.attempt( guess ); 39 | 40 | gameRepository.update(game); 41 | return GuessResult.create(score, game.isGameOver()); 42 | } 43 | 44 | private boolean isGameInProgress(Optional currentGame) { 45 | if (currentGame.isEmpty()) { 46 | return false ; 47 | } 48 | 49 | return !currentGame.get().isGameOver(); 50 | } 51 | } -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/adapters/api/GuessHttpResponseMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.api; 2 | 3 | import com.wordz.domain.GuessResult; 4 | import com.wordz.domain.Score; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | 12 | class GuessHttpResponseMapperTest { 13 | private GuessHttpResponse actual; 14 | 15 | @BeforeEach 16 | void setup() { 17 | // Correct, Part correct, Incorrect, Incorrect, Incorrect 18 | Score score = new Score("ABCZZ"); 19 | score.assess("ACBXX"); 20 | 21 | var guessResult = new GuessResult(score, true, true); 22 | 23 | actual = new GuessHttpResponseMapper().from(guessResult); 24 | } 25 | 26 | @Test 27 | void mapsCorrectLetter() { 28 | assertThat(actual.scores().charAt(0)).isEqualTo('C'); 29 | } 30 | 31 | @Test 32 | void mapsPartCorrectLetter() { 33 | assertThat(actual.scores().charAt(1)).isEqualTo('P'); 34 | } 35 | 36 | @Test 37 | void mapsIncorrectLetter() { 38 | assertThat(actual.scores().charAt(4)).isEqualTo('X'); 39 | } 40 | 41 | @Test 42 | void mapsGameOver() { 43 | assertThat(actual.isGameOver()).isTrue(); 44 | } 45 | } -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/adapters/db/PostgresTestDataSource.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import org.postgresql.ds.PGSimpleDataSource; 4 | 5 | public class PostgresTestDataSource extends PGSimpleDataSource { 6 | PostgresTestDataSource () { 7 | setServerNames(new String[]{"localhost"}); 8 | setDatabaseName("wordzdb"); 9 | setCurrentSchema("public"); 10 | setUser("ciuser"); 11 | setPassword("cipassword"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/adapters/db/WordRepositoryPostgresTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.adapters.db; 2 | 3 | import com.github.database.rider.core.api.configuration.DBUnit; 4 | import com.github.database.rider.core.api.configuration.Orthography; 5 | import com.github.database.rider.core.api.connection.ConnectionHolder; 6 | import com.github.database.rider.core.api.dataset.DataSet; 7 | import com.github.database.rider.junit5.api.DBRider; 8 | import com.wordz.domain.WordRepository; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import javax.sql.DataSource; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @DBRider 17 | @DBUnit(caseSensitiveTableNames = true, 18 | caseInsensitiveStrategy= Orthography.LOWERCASE) 19 | public class WordRepositoryPostgresTest { 20 | 21 | private DataSource dataSource; 22 | 23 | @SuppressWarnings("unused") // Used by DBRider framework 24 | private final ConnectionHolder connectionHolder = () -> dataSource.getConnection(); 25 | 26 | /** 27 | * As this method only sets up a connection to the database and does not 28 | * create any stored data, there is a good argument for using the @BeforeAll 29 | * annotation and doing this once for all tests in this class 30 | */ 31 | @BeforeEach 32 | void setupConnection() { 33 | this.dataSource = new PostgresTestDataSource(); 34 | } 35 | 36 | @Test 37 | @DataSet("adapters/data/wordTable.json") 38 | public void fetchesWord() { 39 | WordRepository repository = new WordRepositoryPostgres(dataSource); 40 | 41 | String actual = repository.fetchWordByNumber(27); 42 | 43 | assertThat(actual).isEqualTo("ARISE"); 44 | } 45 | 46 | @Test 47 | @DataSet("adapters/data/threeWords.json") 48 | public void returnsHighestWordNumber() { 49 | WordRepository repository = new WordRepositoryPostgres(dataSource); 50 | 51 | int actual = repository.highestWordNumber(); 52 | 53 | assertThat(actual).isEqualTo(3); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/domain/GuessTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.Optional; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.Mockito.*; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class GuessTest { 17 | private static final Player PLAYER = new Player("player1"); 18 | private static final String CORRECT_WORD = "ARISE"; 19 | private static final String WRONG_WORD = "RXXXX"; 20 | @Mock 21 | private GameRepository gameRepository; 22 | @InjectMocks 23 | private Wordz wordz; 24 | 25 | @Test 26 | void returnsScoreForGuess() { 27 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 28 | 29 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 30 | 31 | Letter firstLetter = result.score().letter(0); 32 | assertThat(firstLetter).isEqualTo(Letter.PART_CORRECT); 33 | } 34 | 35 | @Test 36 | void updatesAttemptNumber() { 37 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 38 | 39 | wordz.assess(PLAYER, WRONG_WORD); 40 | 41 | var game = getUpdatedGameInRepository(); 42 | assertThat(game.getAttemptNumber()).isEqualTo(1); 43 | } 44 | 45 | @Test 46 | void gameOverOnTooManyIncorrectGuesses(){ 47 | int maximumGuesses = 5; 48 | givenGameInRepository( 49 | Game.create(PLAYER, CORRECT_WORD, 50 | maximumGuesses-1)); 51 | 52 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 53 | 54 | assertThat(result.isGameOver()).isTrue(); 55 | } 56 | 57 | @Test 58 | void rejectsGuessAfterGameOver(){ 59 | var game = Game.create(PLAYER, CORRECT_WORD); 60 | game.end(); 61 | givenGameInRepository( game ); 62 | 63 | GuessResult result = wordz.assess(PLAYER, WRONG_WORD); 64 | 65 | assertThat(result.isError()).isTrue(); 66 | } 67 | 68 | @Test 69 | void recordsGameOverOnCorrectGuess(){ 70 | givenGameInRepository(Game.create(PLAYER, CORRECT_WORD)); 71 | 72 | wordz.assess(PLAYER, CORRECT_WORD); 73 | 74 | Game game = getUpdatedGameInRepository(); 75 | assertThat(game.isGameOver()).isTrue(); 76 | } 77 | 78 | private Game getUpdatedGameInRepository() { 79 | ArgumentCaptor argument = ArgumentCaptor.forClass(Game.class); 80 | verify(gameRepository).update(argument.capture()); 81 | return argument.getValue(); 82 | } 83 | 84 | private void givenGameInRepository(Game game) { 85 | when(gameRepository.fetchForPlayer(eq(PLAYER))) 86 | .thenReturn(Optional.of(game)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/domain/NewGameTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.*; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.mockito.ArgumentMatchers.anyInt; 13 | import static org.mockito.Mockito.*; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class NewGameTest { 19 | private static final Player PLAYER = new Player("player1"); 20 | private static final String CORRECT_WORD = "ARISE"; 21 | 22 | @Mock 23 | private GameRepository gameRepository; 24 | @Mock 25 | private WordRepository wordRepository ; 26 | @Mock 27 | private RandomNumbers randomNumbers ; 28 | @InjectMocks 29 | private Wordz wordz; 30 | 31 | @Test 32 | void startsNewGame() { 33 | givenWordToSelect(CORRECT_WORD); 34 | 35 | wordz.newGame(PLAYER); 36 | 37 | Game game = getGameInRepository(); 38 | assertThat(game.getWord()).isEqualTo(CORRECT_WORD); 39 | assertThat(game.getAttemptNumber()).isZero(); 40 | assertThat(game.getPlayer()).isSameAs(PLAYER); 41 | } 42 | 43 | @Test 44 | void cannotRestartGameInProgress() { 45 | when(gameRepository 46 | .fetchForPlayer(eq(PLAYER))) 47 | .thenReturn(Optional.of( 48 | Game.create(PLAYER, CORRECT_WORD))); 49 | var success = wordz.newGame(PLAYER); 50 | 51 | assertThat(success).isFalse(); 52 | } 53 | 54 | private void givenWordToSelect(String wordToSelect) { 55 | int wordNumber = 2; 56 | 57 | when(randomNumbers.next(anyInt())) 58 | .thenReturn(wordNumber); 59 | 60 | when(wordRepository 61 | .fetchWordByNumber(wordNumber)) 62 | .thenReturn(wordToSelect); 63 | } 64 | 65 | private Game getGameInRepository() { 66 | var gameArgument = ArgumentCaptor.forClass(Game.class); 67 | verify(gameRepository) 68 | .create(gameArgument.capture()); 69 | var game = gameArgument.getValue(); 70 | return game; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/domain/PlayerTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class PlayerTest { 8 | 9 | @Test 10 | void twoAreEqual() { 11 | Player a = new Player("same-name-means-equal"); 12 | Player b = new Player("same-name-means-equal"); 13 | 14 | assertThat(a).isEqualTo(b); 15 | } 16 | 17 | @Test 18 | void twoAreNotEqual() { 19 | Player a = new Player("same-name-means-equal"); 20 | Player b = new Player("different-name-means-not-equal"); 21 | 22 | assertThat(a).isNotEqualTo(b); 23 | } 24 | 25 | @Test 26 | void equalHaveSameHashcode() { 27 | Player a = new Player("same-name-means-equal"); 28 | Player b = new Player("same-name-means-equal"); 29 | 30 | assertThat(a.hashCode()).isEqualTo(b.hashCode()); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/domain/WordSelectionTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.when; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class WordSelectionTest { 14 | 15 | private static final int HIGHEST_WORD_NUMBER = 3 ; 16 | private static final int WORD_NUMBER_SHINE = 2 ; 17 | @Mock 18 | private WordRepository repository; 19 | 20 | @Mock 21 | private RandomNumbers random; 22 | 23 | @BeforeEach 24 | void beforeEachTest() { 25 | when(repository.highestWordNumber()).thenReturn(HIGHEST_WORD_NUMBER); 26 | 27 | when(repository.fetchWordByNumber(WORD_NUMBER_SHINE)).thenReturn("SHINE"); 28 | } 29 | 30 | @Test 31 | void selectsWordAtRandom() { 32 | when(random.next(HIGHEST_WORD_NUMBER)).thenReturn(WORD_NUMBER_SHINE); 33 | var selector = new WordSelection(repository, random); 34 | 35 | String actual = selector.chooseRandomWord(); 36 | 37 | assertThat(actual).isEqualTo("SHINE"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter15/src/test/java/com/wordz/domain/WordTest.java: -------------------------------------------------------------------------------- 1 | package com.wordz.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.wordz.domain.Letter.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class WordTest { 9 | @Test 10 | public void oneIncorrectLetter() { 11 | var word = new Word("A"); 12 | var score = word.guess("Z"); 13 | assertScoreForGuess(score, INCORRECT); 14 | } 15 | 16 | @Test 17 | public void oneCorrectLetter() { 18 | var word = new Word("A"); 19 | var score = word.guess("A"); 20 | assertScoreForGuess(score, CORRECT); 21 | } 22 | 23 | @Test 24 | public void secondLetterWrongPosition() { 25 | var word = new Word("AR"); 26 | var score = word.guess("ZA"); 27 | assertScoreForGuess(score, INCORRECT, 28 | PART_CORRECT); 29 | } 30 | 31 | @Test 32 | public void allScoreCombinations() { 33 | var word = new Word("ARI"); 34 | var score = word.guess("ZAI"); 35 | assertScoreForGuess(score, INCORRECT, 36 | PART_CORRECT, 37 | CORRECT); 38 | } 39 | 40 | @Test 41 | void reportsAllCorrect() { 42 | var word = new Word("ARISE"); 43 | var score = word.guess("ARISE"); 44 | assertThat(score.allCorrect()).isTrue(); 45 | } 46 | 47 | @Test 48 | void reportsNotAllCorrect() { 49 | var word = new Word("ARISE"); 50 | var score = word.guess("ARI*E"); 51 | assertThat(score.allCorrect()).isFalse(); 52 | } 53 | 54 | @Test 55 | void accessesLetters() { 56 | var word = new Word("ARISE"); 57 | var score = word.guess("ARI*E"); 58 | assertThat(score.letters()).hasSize(5); 59 | } 60 | 61 | private void assertScoreForGuess(Score score, Letter... expectedScores) { 62 | for (int position = 0; position < expectedScores.length; position++) { 63 | Letter expected = expectedScores[position]; 64 | assertThat(score.letter(position)) 65 | .isEqualTo(expected); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /chapter15/src/test/resources/adapters/data/createGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 0, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter15/src/test/resources/adapters/data/emptyGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /chapter15/src/test/resources/adapters/data/threeWords.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 1, 5 | "word": "ARISE" 6 | }, 7 | { 8 | "word_number": 2, 9 | "word": "SHINE" 10 | }, 11 | { 12 | "word_number": 3, 13 | "word": "SLEEP" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /chapter15/src/test/resources/adapters/data/updatedGame.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": [ 3 | { 4 | "player_name": "player1", 5 | "word" : "BONUS", 6 | "attempt_number": 1, 7 | "is_game_over": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /chapter15/src/test/resources/adapters/data/wordTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "word": [ 3 | { 4 | "word_number": 27, 5 | "word": "ARISE" 6 | } 7 | ] 8 | } --------------------------------------------------------------------------------