13

Driving the Domain Layer

We laid a lot of groundwork in previous chapters, covering a mixture of TDD techniques and software design approaches. Now we can apply those capabilities to build our Wordz game. We will be building on top of the useful code we have written throughout the book and working toward a well-engineered, well-tested design, written using the test-first approach.

Our goal for this chapter is to create the domain layer of our system. We will adopt the hexagonal architecture approach as described in Chapter 9, Hexagonal Architecture – Decoupling External Systems. The domain model will contain all our core application logic. This code will not be tied to details of any external system technologies such as SQL databases or web servers. We will create abstractions for these external systems and use test doubles to enable us to test-drive the application logic.

Using hexagonal architecture in this way allows us to write FIRST unit tests for complete user stories, which is something often requiring integration or end-to-end testing in other design approaches. We will write our domain model code by applying the ideas presented in the book so far.

In this chapter, we’re going to cover the following main topics:

  • Starting a new game
  • Playing the game
  • Ending the game

Technical requirements

The final code for this chapter can be found at https://github.com/PacktPublishing/Test-Driven-Development-with-Java/tree/main/chapter13.

Starting a new game

In this section, we will make a start by coding our game. Like every project, starting is usually quite difficult, with the first decision being simply where to begin. A reasonable approach is to find a user story that will begin to flesh out the structure of the code. Once we have a reasonable structure for an application, it becomes much easier to figure out where new code should be added.

Given this, we can make a good start by considering what needs to happen when we start a new game. This must set things up ready to play and so will force some critical decisions to be made.

The first user story to work on is starting a new game:

  • As a player I want to start a new game so that I have a new word to guess

When we start a new game, we must do the following:

  1. Select a word at random from the available words to guess
  2. Store the selected word so that scores for guesses can be calculated
  3. Record that the player may now make an initial guess

We will assume the use of hexagonal architecture as we code this story, meaning that any external system will be represented by a port in the domain model. With this in mind, we can create our first test and take it from there.

Test-driving starting a new game

In terms of a general direction, using hexagonal architecture means we are free to use an outside-in approach with TDD. Whatever design we come up with for our domain model, none of it is going to involve difficult-to-test external systems. Our unit tests are assured to be FIRSTfast, isolated, repeatable, self-checking, and timely.

Importantly, we can write unit tests that cover the entire logic needed for a user story. If we wrote code that is bound to external systems – for example, it contained SQL statements and connected to a database – we would need an integration test to cover a user story. Our choice of hexagonal architecture frees us from that.

On a tactical note, we will reuse classes that we have already test-driven, such as class WordSelection, class Word, and class Score. We will reuse existing code and third-party libraries whenever an opportunity presents itself.

Our starting point is to write a test to capture our design decisions related to starting a new game:

  1. We will start with a test called NewGameTest. This test will act across the domain model to drive out our handling of everything we need to do to start a new game:
    package com.wordz.domain;
    public class NewGameTest {
    }
  2. For this test, we will start with the Act step first. We are assuming hexagonal architecture, so the design goal of the Act step is to design the port that handles the request to start a new game. In hexagonal architecture, a port is the piece of code that allows some external system to connect with the domain model. We begin by creating a class for our port:
    package com.wordz.domain;
    public class NewGameTest {
        void startsNewGame() {
            var game = new Game();
        }
    }

The key design decision here is to create a controller class to handle the request to start a game. It is a controller in the sense of the original Gang of Four’s Design Patterns book – a domain model object that will orchestrate other domain model objects. We will let the IntelliJ IDE create the empty Game class:

package com.wordz.domain;
public class Game {
}

That’s another advantage of TDD. When we write the test first, we give our IDE enough information to be able to generate boilerplate code for us. We enable the IDE autocomplete feature to really help us. If your IDE cannot autogenerate code after having written the test, consider upgrading your IDE.

  1. The next step is to add a start() method on the controller class to start a new game. We need to know which player we are starting a game for, so we pass in a Player object. We write the Act step of our test:
    public class NewGameTest {
        @Test
        void startsNewGame() {
            var game = new Game();
            var player = new Player();
            game.start(player);
        }
    }

We allow the IDE to generate the method in the controller:

public class Game {
    public void start(Player player) {
    }
}

Tracking the progress of the game

The next design decisions concern the expected outcome of starting a new game for a player. There are two things that need to be recorded:

  • The selected word that the player attempts to guess
  • That we expect their first guess next

The selected word and current attempt number will need to persist somewhere. We will use the repository pattern to abstract that. Our repository will need to manage some domain objects. Those objects will have the single responsibility of tracking our progress in a game.

Already, we see a benefit of TDD in terms of rapid design feedback. We haven’t written too much code yet, but already, it seems like the new class needed to track game progress would best be called class Game. However, we already have a class Game, responsible for starting a new game. TDD is providing feedback on our design – that our names and responsibilities are mismatched.

We must choose one of the following options to proceed:

  • Keep our existing class Game as it is. Call this new class something such as Progress or Attempt.
  • Change the start() method to a static method – a method that applies to all instances of a class.
  • Rename class Game to something that better describes its responsibility. Then, we can create a new class Game to hold current player progress.

The static method option is unappealing. When using object-oriented programming in Java, static methods rarely seem as good a fit as simply creating another object that manages all the relevant instances. The static method becomes a normal method on this new object. Using class Game to represent progress through a game seems to result in more descriptive code. Let’s go with that approach.

  1. Use the IntelliJ IDEA IDE to refactor/rename class Game class Wordz, which represents the entry point into our domain model. We also rename the local variable game to match:
    public class NewGameTest {
        @Test
        void startsNewGame() {
            var wordz = new Wordz();
            var player = new Player();
            wordz.start(player);
        }
    }

The name of the NewGameTest test is still good. It represents the user story we are testing and is not related to any class names. The production code has been refactored by the IDE as well:

public class Wordz {
    public void start(Player player) {
    }
}
  1. Use the IDE to refactor/rename the start() method newGame(). This seems to better describe the responsibility of the method, in the context of a class named Wordz:
    public class NewGameTest {
        @Test
        void startsNewGame() {
            var wordz = new Wordz();
            var player = new Player();
            wordz.newGame(player);
        }
    }

The class Wordz production code also has the method renamed.

  1. When we start a new game, we need to select a word to guess and start the sequence of attempts the player has. These facts need to be stored in a repository. Let’s create the repository first. We will call it interface GameRepository and add Mockito @Mock support for it in our test:
    package com.wordz.domain;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.Mock;
    import org.mockito.junit.jupiter.MockitoExtension;
    @ExtendWith(MockitoExtension.class)
    public class NewGameTest {
        @Mock
        private GameRepository gameRepository;
        @InjectMocks
        private Wordz wordz;
        @Test
        void startsNewGame() {
            var player = new Player();
            wordz.newGame(player);
        }
    }

We add the @ExtendWith annotation to the class to enable the Mockito library to automatically create test doubles for us. We add a gameRepository field, which we annotated as a Mockito @Mock. We use the @InjectMocks convenience annotation built into Mockito to automatically inject this dependency into the Wordz constructor.

  1. We allow the IDE to create an empty interface for us:
    package com.wordz.domain;
    public interface GameRepository {
    }
  2. For the next step, we will confirm that gameRepository gets used. We decide to add a create() method on the interface, which takes a class Game object instance as its only parameter. We want to inspect that object instance of class Game, so we add an argument captor. This allows us to assert on the game data contained in that object:
    public class NewGameTest {
        @Mock
        private GameRepository gameRepository;
        @Test
        void startsNewGame() {
            var player = new Player();
            wordz.newGame(player);
            var gameArgument =
                   ArgumentCaptor.forClass(Game.class)
            verify(gameRepository)
               .create(gameArgument.capture());
            var game = gameArgument.getValue();
            assertThat(game.getWord()).isEqualTo("ARISE");
            assertThat(game.getAttemptNumber()).isZero();
            assertThat(game.getPlayer()).isSameAs(player);
        }
    }

A good question is why we are asserting against those particular values. The reason is that we are going to cheat when we add the production code and fake it until we make it. We will return a Game object that hardcodes these values as a first step. We can then work in small steps. Once the cheat version makes the test pass, we can refine the test and test-drive the code to fetch the word for real. Smaller steps provide more rapid feedback. Rapid feedback enables better decision-making.

Note on using getters in the domain model

The Game class has getXxx() methods, known as getters in Java terminology, for every one of its private fields. These methods break the encapsulation of data.

This is generally not recommended. It can lead to important logic being placed into other classes – a code smell known as a foreign method. Object-oriented programming is all about co-locating logic and data, encapsulating both. Getters should be few and far between. That does not mean we should never use them, however.

In this case, the single responsibility of class Game is to transfer the current state of the game being played to GameRepository. The most direct way of implementing this is to add getters to the class. Writing simple, clear code beats following rules dogmatically.

Another reasonable approach is to add a getXxx() diagnostic method at package-level visibility purely for testing. Check with the team that this is not part of the public API and do not use it in production code. It is more important to get the code correct than obsess over design trivia.

  1. We create empty methods for these new getters using the IDE. The next step is to run NewGameTest and confirm that it fails:
Figure 13.1 – Our failing test

Figure 13.1 – Our failing test

  1. This is enough for us to write some more production code:
    package com.wordz.domain;
    public class Wordz {
        private final GameRepository gameRepository;
        public Wordz(GameRepository gr) {
            this.gameRepository = gr;
        }
        public void newGame(Player player) {
            var game = new Game(player, "ARISE", 0);
            gameRepository.create(game);
        }
    }

We can rerun NewGameTest and watch it pass:

Figure 13.2 – The test passes

Figure 13.2 – The test passes

The test now passes. We can move from our red-green phase to thinking about refactoring. The thing that jumps out immediately is just how unreadable that ArgumentCaptor code is in the test. It contains too much detail about the mechanics of mocking and not enough detail about why we are using that technique. We can clarify that by extracting a well-named method.

  1. Extract the getGameInRepository() method for clarity:
    @Test
    void startsNewGame() {
        var player = new Player();
        wordz.newGame(player);
        Game game = getGameInRepository();
        assertThat(game.getWord()).isEqualTo("ARISE");
        assertThat(game.getAttemptNumber()).isZero();
        assertThat(game.getPlayer()).isSameAs(player);
    }
    private Game getGameInRepository() {
        var gameArgument
           = ArgumentCaptor.forClass(Game.class)
        verify(gameRepository)
                .create(gameArgument.capture());
        return gameArgument.getValue();
    }

That has made the test much simpler to read and see the usual Arrange, Act, and Assert pattern in it. It is a simple test by nature and should read as such. We can now rerun the test and confirm that it still passes. It does, and we are satisfied that our refactoring did not break anything.

That completes our first test – a job well done! We’re making good progress here. It always feels good to me to see a test go green, and that feeling never gets old. This test is essentially an end-to-end test of a user story, acting only on the domain model. Using hexagonal architecture enables us to write tests that cover the details of our application logic, while avoiding the need for test environments. We get faster-running, more stable tests as a result.

There is more work to do in our next test, as we need to remove the hardcoded creation of the Game object. In the next section, we will address this by triangulating the word selection logic. We design the next test to drive out the correct behavior of selecting a word at random.

Triangulating word selection

The next task is to remove the cheating that we used to make the previous test pass. We hardcoded some data when we created a Game object. We need to replace that with the correct code. This code must select a word at random from our repository of known five-letter words.

  1. Add a new test to drive out the behavior of selecting a random word:
        @Test
        void selectsRandomWord() {
        }
  2. Random word selection depends on two external systems – the database that holds the words to choose from and a source of random numbers. As we are using hexagonal architecture, the domain layer cannot access those directly. We will represent them with two interfaces – the ports to those systems. For this test, we will use Mockito to create stubs for those interfaces:
    @ExtendWith(MockitoExtension.class)
    public class NewGameTest {
        @Mock
        private GameRepository gameRepository;
        @Mock
        private WordRepository wordRepository ;
        @Mock
        private RandomNumbers randomNumbers ;
        @InjectMocks
        private Wordz wordz;

This test introduces two new collaborating objects to class Wordz. These are instances of any valid implementations of both interface WordRepository and interface RandomNumbers. We need to inject those objects into the Wordz object to make use of them.

  1. Using dependency injection, inject the two new interface objects into the class Wordz constructor:
    public class Wordz {
        private final GameRepository gameRepository;
        private final WordSelection wordSelection ;
        public Wordz(GameRepository gr,
                     WordRepository wr,
                     RandomNumbers rn) {
            this.gameRepository = gr;
            this.wordSelection = new WordSelection(wr, rn);
        }

We’ve added two parameters to the constructor. We do not need to store them directly as fields. Instead, we use the previously created class WordSelection. We create a WordSelection object and store it in a field called wordSelection. Note that our earlier use of @InjectMocks means that our test code will automatically pass in the mock objects to this constructor, without further code changes. It is very convenient.

  1. We set up the mocks. We want them to simulate the behavior we expect from interface WordRepository when we call the fetchWordByNumber() method and interface RandomNumbers when we call next():
        @Test
        void selectsRandomWord() {
            when(randomNumbers.next(anyInt())).thenReturn(2);
            when(wordRepository.fetchWordByNumber(2))
                   .thenReturn("ABCDE");
        }

This will set up our mocks so that when next() is called, it will return the word number 2 every time, as a test double for the random number that will be produced in the full application. When fetchWordByNumber() is then called with 2 as an argument, it will return the word with word number 2, which will be "ABCDE" in our test. Looking at that code, we can add clarity by using a local variable instead of that magic number 2. To future readers of the code, the link between random number generator output and word repository will be more obvious:

    @Test
    void selectsRandomWord() {
        int wordNumber = 2;
        when(randomNumbers.next(anyInt()))
           .thenReturn(wordNumber);
        when(wordRepository
           .fetchWordByNumber(wordNumber))
               .thenReturn("ABCDE");
    }
  1. That still looks too detailed once again. There is too much emphasis on mocking mechanics and too little on what the mocking represents. Let’s extract a method to explain why we are setting up this stub. We will also pass in the word we want to be selected. That will help us more easily understand the purpose of the test code:
        @Test
        void selectsRandomWord() {
            givenWordToSelect("ABCDE");
        }
        private void givenWordToSelect(String wordToSelect){
            int wordNumber = 2;
            when(randomNumbers.next(anyInt()))
                    .thenReturn(wordNumber);
            when(wordRepository
                    .fetchWordByNumber(wordNumber))
                    .thenReturn(wordToSelect);
        }
  2. Now, we can write the assertion to confirm that this word is passed down to the gameRepository create() method – we can reuse our getGameInRepository() assert helper method:
    @Test
    void selectsRandomWord() {
        givenWordToSelect("ABCDE");
        var player = new Player();
        wordz.newGame(player);
        Game game = getGameInRepository();
        assertThat(game.getWord()).isEqualTo("ABCDE");
    }

This follows the same approach as the previous test, startsNewGame.

  1. Watch the test fail. Write production code to make the test pass:
    public void newGame(Player player) {
        var word = wordSelection.chooseRandomWord();
        Game game = new Game(player, word, 0);
        gameRepository.create(game);
    }
  2. Watch the new test pass and then run all tests:
Figure 13.3 – Original test failing

Figure 13.3 – Original test failing

Our initial test has now failed. We’ve broken something during our latest code change. TDD has kept us safe by providing a regression test for us. What has happened is that after removing the hardcoded word "ARISE" that the original test relied on, it fails. The correct solution is to add the required mock setup to our original test. We can reuse our givenWordToSelect() helper method to do this.

  1. Add the mock setup to the original test:
    @Test
    void startsNewGame() {
        var player = new Player();
        givenWordToSelect("ARISE");
        wordz.newGame(player);
        Game game = getGameInRepository();
        assertThat(game.getWord()).isEqualTo("ARISE");
        assertThat(game.getAttemptNumber()).isZero();
        assertThat(game.getPlayer()).isSameAs(player);
    }
  2. Rerun all tests and confirm that they all pass:
Figure 13.4 – All tests passing

Figure 13.4 – All tests passing

We’ve test-driven our first piece of code to start a new game, with a randomly selected word to guess, and made the tests pass. Before we move on, it is time to consider what – if anything – we should refactor. We have been tidying the code as we write it, but there is one glaring feature. Take a look at the two tests. They seem very similar now. The original test has become a superset of the one we used to test-drive adding the word selection. The selectsRandomWord() test is a scaffolding test that no longer serves a purpose. There’s only one thing to do with code like that – remove it. As a minor readability improvement, we can also extract a constant for the Player variable:

  1. Extract a constant for the Player variable:
private static final Player PLAYER = new Player();
@Test
void startsNewGame() {
    givenWordToSelect("ARISE");
    wordz.newGame(PLAYER);
    Game game = getGameInRepository();
    assertThat(game.getWord()).isEqualTo("ARISE");
    assertThat(game.getAttemptNumber()).isZero();
    assertThat(game.getPlayer()).isSameAs(PLAYER);
}
  1. We’ll run all the tests after this to make sure that they all still pass and that selectsRandomWord() has gone.
Figure 13.5 – All tests passing

Figure 13.5 – All tests passing

That’s it! We have test-driven out all the behavior we need to start a game. It’s a significant achievement because that test covers a complete user story. All the domain logic has been tested and is known to be working. The design looks straightforward. The test code is a clear specification of what we expect our code to do. This is great progress.

Following this refactoring, we can move on to the next development task – code that supports playing the game.

Playing the game

In this section, we will build the logic to play the game. The gameplay consists of making a number of guesses at the selected word, reviewing the score for that guess, and having another guess. The game ends either when the word has been guessed correctly or when the maximum number of allowed attempts has been made.

We’ll begin by assuming that we are at the start of a typical game, about to make our first guess. We will also assume that this guess is not completely correct. This allows us to defer decisions about end-of-the-game behavior, which is a good thing, as we have enough to decide already.

Designing the scoring interface

The first design decision we must take is what we need to return following a guess at the word. We need to return the following information to the user:

  • The score for the current guess
  • Whether or not the game is still in play or has ended
  • Possibly the previous history of scoring for each guess
  • Possibly a report of user input errors

Clearly, the most important information for the player is the score for the current guess. Without that, the game cannot be played. As the game has a variable length – ending when either the word has been guessed, or when a maximum number of guesses has been attempted – we need an indicator that another guess will be allowed.

The idea behind returning the history of scores for previous guesses is that it might help the consumer of our domain model – ultimately, a user interface of some sort. If we return only the score for the current guess, the user interface will most likely need to retain its own history of scores, in order to present them properly. If we return the entire history of scores for this game, that information is easily available. A good rule of thumb in software is to follow the you ain’t gonna need it (YAGNI) principle. As there is no requirement for a history of scores, we won’t build that at this stage.

The last decision we need to write our test is to think about the programming interface we want for this. We will choose an assess() method on class Wordz. It will accept String, which is the current guess from the player. It will return record, which is a modern Java (since Java 14) way of indicating a pure data structure is to be returned:

We've now got enough to write a test. We'll make a new test for all guess-related behavior called class GuessTest. The test looks like this:

@ExtendWith(MockitoExtension.class)
public class GuessTest {
    private static final Player PLAYER = new Player();
    private static final String CORRECT_WORD = "ARISE";
    private static final String WRONG_WORD = "RXXXX";
    @Mock
    private GameRepository gameRepository;
    @InjectMocks
    private Wordz wordz;
    @Test
    void returnsScoreForGuess() {
        givenGameInRepository(
                       Game.create(PLAYER, CORRECT_WORD));
        GuessResult result = wordz.assess(PLAYER, WRONG_WORD);
        Letter firstLetter = result.score().letter(0);
        assertThat(firstLetter)
               .isEqualTo(Letter.PART_CORRECT);
    }
    private void givenGameInRepository(Game game) {
        when(gameRepository
           .fetchForPlayer(eq(PLAYER)))
              .thenReturn(Optional.of(game));
    }
}

There are no new TDD techniques in the test. It drives out the calling interface for our new assess() method. We’ve used the static constructor idiom to create the game object using Game.create(). This method has been added to class Game:

    static Game create(Player player, String correctWord) {
        return new Game(player, correctWord, 0, false);
    }

This clarifies the information necessary to create a new game. To get the test to compile, we create record GuessResult:

package com.wordz.domain;
import java.util.List;
public record GuessResult(
        Score score,
        boolean isGameOver
) { }

We can make the test pass by writing the production code for the assess() method in class Wordz. To do that, we will reuse the class Word class that we have already written:

public GuessResult assess(Player player, String guess) {
    var game = gameRepository.fetchForPlayer(player);
    var target = new Word(game.getWord());
    var score = target.guess(guess);
    return new GuessResult(score, false);
}

The assertion checks only that the score for the first letter is correct. This is intentionally a weak test. The detailed testing for scoring behavior is done in class WordTest, which we wrote previously. The test is described as weak, as it does not fully test the returned score, only the first letter of it. Strong testing of the scoring logic happens elsewhere, in class WordTest. The weak test here confirms we have something capable of scoring at least one letter correctly and is enough for us to test-drive the production code. We avoid duplicating tests here.

Running the test shows that it passes. We can review the test code and production code to see whether refactoring will improve their design. At this point, nothing needs our urgent attention. We can move on to tracking progress through the game.

Triangulating game progress tracking

We need to track the number of guesses that have been made so that we can end the game after a maximum number of attempts. Our design choice is to update the attemptNumber field in the Game object and then store it in GameRepository:

  1. We add a test to drive this code out:
    @Test
    void updatesAttemptNumber() {
        givenGameInRepository(
                   Game.create(PLAYER, CORRECT_WORD));
        wordz.assess(PLAYER, WRONG_WORD);
        var game = getUpdatedGameInRepository();
        assertThat(game.getAttemptNumber()).isEqualTo(1);
    }
    private Game getUpdatedGameInRepository() {
        ArgumentCaptor<Game> argument
                = ArgumentCaptor.forClass(Game.class);
        verify(gameRepository).update(argument.capture());
        return argument.getValue();
    }

This test introduces a new method, update(), into our interface GameRepository, responsible for writing the latest game information to storage. The Assert step uses a Mockito ArgumentCaptor to inspect the Game object that we pass into update(). We have written a getUpdatedGameInRepository() method to deemphasize the inner workings of how we check what was passed to the gameRepository.update() method. assertThat() in the test verifies that attemptNumber has been incremented. It started at zero, due to us creating a new game, and so the expected new value is 1. This is the desired behavior for tracking an attempt to guess the word:

  1. We add the update() method to the GameRepository interface:
    package com.wordz.domain;
    public interface GameRepository {
        void create(Game game);
        Game fetchForPlayer(Player player);
        void update(Game game);
    }
  2. We add the production code to the assess() method in class Wordz to increment attemptNumber and call update():
    public GuessResult assess(Player player, String guess) {
        var game = gameRepository.fetchForPlayer(player);
        game.incrementAttemptNumber();
        gameRepository.update(game);
        var target = new Word(game.getWord());
        var score = target.guess(guess);
        return new GuessResult(score, false);
    }
  3. We add the incrementAttemptNumber() method to class Game:
    public void incrementAttemptNumber() {
        attemptNumber++;
    }

The test now passes. We can think about any refactoring improvements we want to make. There are two things that seem to stand out:

  • The duplicated test setup between class NewGameTest and class GuessTest.

At this stage, we can live with this duplication. The options are to combine both tests into the same test class, to extend a common test base class, or to use composition. None of them seem likely to aid readability much. It seems quite nice to have the two different test cases separate for now.

  • The three lines inside the assess() method must always be called as a unit when we attempt another guess. It is possible to forget to call one of these, so it seems better to refactor to eliminate that possible error. We can refactor like this:
    public GuessResult assess(Player player, String guess) {
        var game = gameRepository.fetchForPlayer(player);
        Score score = game.attempt( guess );
        gameRepository.update(game);
        return new GuessResult(score, false);
    }

We move the code that used to be here into the newly created method: attempt() on class Game:

public Score attempt(String latestGuess) {
    attemptNumber++;
    var target = new Word(targetWord);
    return target.guess(latestGuess);
}

Renaming the method argument from guess to latestGuess improves readability.

That completes the code needed to take a guess at the word. Let’s move on to test-driving the code we will need to detect when a game has ended.

Ending the game

In this section, we will complete the tests and production code we need to drive out detecting the end of a game. This will happen when we do either of the following:

  • Guess the word correctly
  • Make our final allowed attempt, based on a maximum number

We can make a start by coding the end-of-game detection when we guess the word correctly.

Responding to a correct guess

In this case, the player guesses the target word correctly. The game is over, and the player is awarded a number of points, based on how few attempts were needed before the correct guess was made. We need to communicate that the game is over and how many points have been awarded, leading to two new fields in our class GuessResult. We can add a test to our existing class GuessTest as follows:

@Test
void reportsGameOverOnCorrectGuess(){
    var player = new Player();
    Game game = new Game(player, "ARISE", 0);
    when(gameRepository.fetchForPlayer(player))
                          .thenReturn(game);
    var wordz = new Wordz(gameRepository,
                           wordRepository, randomNumbers);
    var guess = "ARISE";
    GuessResult result = wordz.assess(player, guess);
    assertThat(result.isGameOver()).isTrue();
}

This drives out both a new isGameOver()accessor in class GuessResult and the behavior to make that true:

public GuessResult assess(Player player, String guess) {
    var game = gameRepository.fetchForPlayer(player);
    Score score = game.attempt( guess );
    if (score.allCorrect()) {
        return new GuessResult(score, true);
    }
    gameRepository.update(game);
    return new GuessResult(score, false);
}

This itself drives out two new tests in class WordTest:

@Test
void reportsAllCorrect() {
    var word = new Word("ARISE");
    var score = word.guess("ARISE");
    assertThat(score.allCorrect()).isTrue();
}
@Test
void reportsNotAllCorrect() {
    var word = new Word("ARISE");
    var score = word.guess("ARI*E");
    assertThat(score.allCorrect()).isFalse();
}

These themselves drive out an implementation in class Score:

public boolean allCorrect() {
    var totalCorrect = results.stream()
            .filter(letter -> letter == Letter.CORRECT)
            .count();
    return totalCorrect == results.size();
}

With this, we have a valid implementation for the isGameOver accessor in record GuessResult. All tests pass. Nothing seems to need refactoring. We’ll move on to the next test.

Triangulating the game over due to too many incorrect guesses

The next test will drive out the response to exceeding the maximum number of guesses allowed in a game:

@Test
void gameOverOnTooManyIncorrectGuesses(){
    int maximumGuesses = 5;
    givenGameInRepository(
            Game.create(PLAYER, CORRECT_WORD,
                    maximumGuesses-1));
    GuessResult result = wordz.assess(PLAYER, WRONG_WORD);
    assertThat(result.isGameOver()).isTrue();
}

This test sets up gameRepository to allow one, final guess. It then sets up the guess to be incorrect. We assert that isGameOver() is true in this case. The test fails initially, as desired. We add an extra static constructor method in class Game to specify an initial number of attempts.

We add the production code to end the game based on a maximum number of guesses:

public GuessResult assess(Player player, String guess) {
    var game = gameRepository.fetchForPlayer(player);
    Score score = game.attempt( guess );
    if (score.allCorrect()) {
        return new GuessResult(score, true);
    }
    gameRepository.update(game);
    return new GuessResult(score,
                           game.hasNoRemainingGuesses());
}

We add this decision support method to class Game:

public boolean hasNoRemainingGuesses() {
    return attemptNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES;
}

All our tests now pass. There is something suspicious about the code, however. It has been very finely tuned to work only if a guess is correct and within the allowed number of guesses, or when the guess is incorrect and exactly at the allowed number. It’s time to add some boundary condition tests and double-check our logic.

Triangulating response to guess after game over

We need a couple more tests around the boundary conditions of the game over detection. The first one drives out the response to an incorrect guess being submitted after a correct guess:

@Test
void rejectsGuessAfterGameOver(){
    var gameOver = new Game(PLAYER, CORRECT_WORD,
                1, true);
    givenGameInRepository( gameOver );
    GuessResult result = wordz.assess(PLAYER, WRONG_WORD);
    assertThat(result.isError()).isTrue();
}

There are a couple of design decisions captured in this test:

  • Once the game ends, we record this in a new field, isGameOver, in class Game.
  • This new field will need to be set whenever the game ends. We will need more tests to drive that behavior out.
  • We will use a simple error-reporting mechanism – a new field, isError, in class GuessResult.

This leads to a bit of automated refactoring to add the fourth parameter to the class Game constructor. Then, we can add code to make the test pass:

public GuessResult assess(Player player, String guess) {
    var game = gameRepository.fetchForPlayer(player);
    if(game.isGameOver()) {
        return GuessResult.ERROR;
    }
    Score score = game.attempt( guess );
    if (score.allCorrect()) {
        return new GuessResult(score, true, false);
    }
    gameRepository.update(game);
    return new GuessResult(score,
                   game.hasNoRemainingGuesses(), false);
}

The design decision here is that as soon as we fetch the Game object, we check whether the game was previously marked as being over. If so, we report an error and we’re done. It’s simple and crude but adequate for our purposes. We also add a static constant, GuessResult.ERROR, for readability:

    public static final GuessResult ERROR
                  = new GuessResult(null, true, true);

One consequence of this design decision is that we must update GameRepository whenever the Game.isGameOver field changes to true. An example of one of these tests is this:

@Test
void recordsGameOverOnCorrectGuess(){
    givenGameInRepository(Game.create(PLAYER, CORRECT_WORD));
    wordz.assess(PLAYER, CORRECT_WORD);
    Game game = getUpdatedGameInRepository();
    assertThat(game.isGameOver()).isTrue();
}

Here is the production code to add that recording logic:

public GuessResult assess(Player player, String guess) {
    var game = gameRepository.fetchForPlayer(player);
    if(game.isGameOver()) {
        return GuessResult.ERROR;
    }
    Score score = game.attempt( guess );
    if (score.allCorrect()) {
        game.end();
        gameRepository.update(game);
        return new GuessResult(score, true, false);
    }
    gameRepository.update(game);
    return new GuessResult(score,
                 game.hasNoRemainingGuesses(), false);
}

We need another test to drive out the recording of game over when we run out of guesses. That will lead to a change in the production code. Those changes can be found in GitHub at the link given at the start of this chapter. They are very similar to the ones made previously.

Finally, let’s review our design and see whether we can improve it still further.

Reviewing our design

We’ve been making small, tactical refactoring steps as we write the code, which is always a good idea. Like gardening, it is far easier to keep the garden tidy if we pull up weeds before they grow. Even so, it is worth taking a holistic look at the design of our code and tests before we move on. We may never get the chance to touch this code again, and it has our name on it. Let’s make it something that we are proud of and that will be safe and simple for our colleagues to work with in the future.

The tests we’ve already written enable us great latitude in refactoring. They have avoided testing specific implementations, instead testing desired outcomes. They also test larger units of code – in this case, the domain model of our hexagonal architecture. As a result, without changing any tests, it is possible to refactor our class Wordz to look like this:

package com.wordz.domain;
public class Wordz {
    private final GameRepository gameRepository;
    private final WordSelection selection ;
    public Wordz(GameRepository repository,
                 WordRepository wordRepository,
                 RandomNumbers randomNumbers) {
        this.gameRepository = repository;
        this.selection =
             new WordSelection(wordRepository, randomNumbers);
    }
    public void newGame(Player player) {
        var word = wordSelection.chooseRandomWord();
        gameRepository.create(Game.create(player, word));
    }

Our refactored assess() method now looks like this:

    public GuessResult assess(Player player, String guess) {
        Game game = gameRepository.fetchForPlayer(player);
        if(game.isGameOver()) {
            return GuessResult.ERROR;
        }
        Score score = game.attempt( guess );
        gameRepository.update(game);
        return new GuessResult(score,
                               game.isGameOver(), false);
    }
}

That’s looking simpler. The class GuessResult constructor code now stands out as being particularly ugly. It features the classic anti-pattern of using multiple Boolean flag values. We need to clarify what the different combinations actually mean, to simplify creating the object. One useful approach is to apply the static constructor idiom once more:

package com.wordz.domain;
public record GuessResult(
        Score score,
        boolean isGameOver,
        boolean isError
) {
    static final GuessResult ERROR
         = new GuessResult(null, true, true);
    static GuessResult create(Score score,
                              boolean isGameOver) {
        return new GuessResult(score, isGameOver, false);
    }
}

This simplifies the assess() method by eliminating the need to understand that final Boolean flag:

public GuessResult assess(Player player, String guess) {
    Game game = gameRepository.fetchForPlayer(player);
    if(game.isGameOver()) {
        return GuessResult.ERROR;
    }
    Score score = game.attempt( guess );
    gameRepository.update(game);
    return GuessResult.create(score, game.isGameOver());
}

Another improvement to aid understanding concerns creating new instances of class Game. The rejectsGuessAfterGameOver() test uses Boolean flag values in a four-argument constructor to set the test up in a game-over state. Let’s make the goal of creating a game-over state explicit. We can make the Game constructor private, and increase the visibility of the end() method, which is already used to end a game. Our revised test looks like this:

@Test
void rejectsGuessAfterGameOver(){
    var game = Game.create(PLAYER, CORRECT_WORD);
    game.end();
    givenGameInRepository( game );
    GuessResult result = wordz.assess(PLAYER, WRONG_WORD);
    assertThat(result.isError()).isTrue();
}

The Arrange step is now more descriptive. The four-argument constructor is no longer accessible, steering future development to use the safer, more descriptive static constructor methods. This improved design helps prevent defects from being introduced in the future.

We have made great progress in this chapter. Following these final refactoring improvements, we have an easily readable description of the core logic of our game. It is fully backed by FIRST unit tests. We have even achieved a meaningful 100% code coverage of lines of code executed by our tests. This is shown in the IntelliJ code coverage tool:

Figure 13.6 – Code coverage report

Figure 13.6 – Code coverage report

That’s the core of our game finished. We can start a new game, play a game, and end a game. The game can be developed further to include features such as awarding a points score based on how quickly the word was guessed and a high score table for players. These would be added using the same techniques we have been applying throughout this chapter.

Summary

We’ve covered a lot of ground in this chapter. We have used TDD to drive out the core application logic for our Wordz game. We have taken small steps and used triangulation to steadily drive more details into our code implementation. We have used hexagonal architecture to enable us to use FIRST unit tests, freeing us from cumbersome integration tests with their test environments. We have employed test doubles to replace difficult-to-control objects, such as the database and random number generation.

We built up a valuable suite of unit tests that are decoupled from specific implementations. This enabled us to refactor the code freely, ending up with a very nice software design, based on the SOLID principles, which will reduce maintenance efforts significantly.

We finished with a meaningful code coverage report that showed 100% of the lines of production code were executed by our tests, giving us a high degree of confidence in our work.

Next, in Chapter 14, Driving the Database Layer, we will write the database adapter along with an integration test to implement our GameRepository, using the Postgres database.

Questions and answers

  1. Does every method in every class have to have its own unit test?

No. That seems to be a common view, but it is harmful. If we use that approach, we are locking in the implementation details and will not be able to refactor without breaking tests.

  1. What is the significance of 100% code coverage when running our tests?

Not much, by itself. It simply means that all the lines of code in the units under the test were executed during the test run. For us, it means a little more due to our use of test-first TDD. We know that every line of code was driven by a meaningful test of behavior that is important to our application. Having 100% coverage is a double-check that we didn’t forget to add a test.

  1. Does 100% code coverage during the test run mean we have perfect code?

No. Testing can only reveal the presence of defects, never their absence. We can have 100% coverage with very low-quality code in terms of readability and edge case handling. It is important to not attach too much importance to code coverage metrics. For TDD, they serve as a cross-check that we haven’t missed any boundary condition tests.

  1. Is all this refactoring normal?

Yes. TDD is all about rapid feedback loops. Feedback helps us explore design ideas and change our minds as we uncover better designs. It frees us from the tyranny of having to understand every detail – somehow – before we start work. We discover a design by doing the work and have working software to show for it at the end.

Further reading

  • AssertJ documentation – read more about the various kinds of assertion matchers built into AssertJ, as well as details on how to create custom assertions here: https://assertj.github.io/doc/.
  • Refactoring – Improving the Design of Existing Code, Martin Fowler (first edition), ISBN 9780201485677:

The bulk of our work in TDD is refactoring code, continuously providing a good-enough design to support our new features. This book contains excellent advice on how to approach refactoring in a disciplined, step-by-step way.

The first edition of the book uses Java for all its examples, so is more useful to us than the JavaScript-based second edition.

  • Design Patterns – Elements of Reusable Object-Oriented Software, Gamma, Helm, Vlissides, Johnson, ISBN 9780201633610:

A landmark book that cataloged common combinations of classes that occur in object-oriented software. Earlier in the chapter, we used a controller class. This is described as a façade pattern, in the terms of this book. The listed patterns are free of any kind of framework or software layer and so are very useful in building the domain model of hexagonal architecture.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.149.94