Welcome back to the Definitive Guide to Unit Testing! In Chapter 1, we wrote a single unit test for our example code, which allows people to make a move and check whether someone has won a game of Tic-Tac-Toe. In Chapter 2, we generated a code coverage report to see how much of the code we covered with our single test case.
In this chapter, we will address how to create a complete test suite for this code. The question is, what is a complete test suite? As discussed in Chapter 2, we could say that a test suite that achieves 80% or more code coverage is complete. But instead, let’s break that down and think about whether we are accurately testing the functionality required from the code.
Let’s dive into the requirements
- Players take turns to make moves in empty squares until someone wins.
- Players can check to see who has won.
Looking at the class board.java, there are four methods:
- setCell
- nextPlayer
- whoHasWon
- getCell
We won’t consider testing nextPlayer, because this is a private method used only by the other methods in the class. We expect to gain sufficient coverage by exercising the other methods. We also don’t need to test getCell directly, because the code for this method is so simple.
For setCell, I am going to consider the following test cases:
- Nought can make a move to an empty cell
- Cross can make a move to an empty cell
- Cannot make a move to an occupied cell
- Same player cannot do two moves in succession
- Cannot make a move once the game is over
For whoHasWon I am going to consider the following tests:
- Noughts can win through each row
- Crosses can win through each column
- Can win through each diagonal
Now is a good time to introduce one of the JUnit rules around exception handling. If a player tries to occupy a cell that is already occupied, then an exception will be thrown. Given this is desirable behavior, we want to test this.
@Rule
public ExpectedException exception = ExpectedException.none();
The above line tells JUnit to create a rule that exceptions should not be thrown as the test is executed. If an exception is thrown during a test without this rule, it will not be marked as passed. So the important part of defining this rule is to use it in the test case to say that we expect an exception.
@Test
public void cannotPlaceMoveInUsedCell() {
// Arrange
Board myBoard = new Board();
Coordinate cell = new Coordinate(0,0);
myBoard.setCell(cell, Player.NOUGHT);
// Act
exception.expect(IllegalArgumentException.class);
myBoard.setCell(cell, Player.CROSS);
}
In this test, we are asking our board to place an X in a cell that’s already occupied. This will generate an IllegalArgumentException. Therefore, immediately before we try to place an X in an occupied cell, we set the rule to expect an IllegalArgumentException; if that exception is not thrown, the test case will fail.
Having written tests for each of the test cases listed earlier, let’s validate our coverage using the code coverage that we set up in the last tutorial. Here are the results:
This result looks good: we have covered all the code in the Board class. However, the Coordinate class has comparatively poor coverage. Let’s look at this a little closer.
This class was added so we could have the check for a valid location on the board in a single place. Nowhere in our tests so far have we checked for a case where the cell provided is invalid.
Let’s create a file CoordinateTest.java and add a test for this. Those of you who have looked at the code carefully will see that we need to add two tests: one with the column greater than 2, and the other with the row greater than 2. For the sake of completeness, let’s also add a test that checks the other side of the boundary, i.e. creating a cell with the max values.
Now we can see that we have a complete set of tests. We are covering all the required functionality and we are hitting all the lines. As always, the code for the tutorial is available on GitLab. Check out Chapter 4 next to learn about mocking!