Now that you’ve created your first unit test, learned how to calculate code coverage with JaCoCo, and started to make a more comprehensive test suite, it’s time to learn a slightly more advanced unit testing skill: mocking.
Mocking is where we substitute the behavior of part of the solution with our own definition for the purposes of testing. You’re probably thinking that using mocks means you will need to maintain the user-facing code and the mock, so what’s the point of making extra work for yourself? Well, you want your test cases to be deterministic, i.e. to generate the same result no matter when or how you run them.
There are a couple of different things that can impact test cases and make them non-deterministic: First is accessing things outside of the program e.g. Network, Files etc. Second is any behavior that is random, or based on date or time.
Mocking: Getting Started
Let’s have a look at an example where we can use mocking. A method has been added to our Tic-Tac-Toe game which will allow the computer to select the next move for the player. This is based on randomly selecting a cell from the list of those currently available:
/**
* Choose a random free cell for my next move.
* @param player to place in the cell.
*/
public void randomMove(Player player) {
// Find all the available cells
ArrayList<Coordinate> availableCells = new ArrayList<>();
for (int i=0; i<3; i++) {
for (int j=0; j<3; j++) {
Coordinate currentCell = new Coordinate(i, j);
if (getCell(currentCell) == null)
}
}
int numOfCells = availableCells.size();
if (numOfCells == 0) {
throw new IllegalArgumentException("Board is full, cannot place in a move");
}
// Pick a random cell
int index = (int)(Math.random() * numOfCells);
// Put player in that cell
setCell(availableCells.get(index), player);
}
So how are we going to test this? Essentially, there are two test cases:
- Player is put into an empty cell
- Correct error when the build is full
If you’ve followed Chapters 1 through 3 of this series, you already know how to write a test for the second. But what about the first? Sure, we can run the method and ensure that an exception isn’t thrown, but how are we going to test that the player has been put in a cell?
The most deterministic approach is to mock the random number generator so that when the test runs, we know which cell is being used. Before we can write the test, we need to include a couple of new dependencies; these are the mocking framework that we are going to use.
Add the following to the dependencies section of our pom.xml:
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
Now that we have the dependencies set up, let’s have a look at this test:
@PrepareForTest({Board.class, Math.class})
@Test
public void randomMoveInputNoughtOutputIndexOutOfBoundsException() {
// Setup mocks
mockStatic(Math.class);
when(Math.random()).thenReturn(0.5);
// Arrange
Board myBoard = new Board();
// Act
myBoard.randomMove(Player.NOUGHT);
// Assert
assertEquals("Player O not in the middle cell", Player.NOUGHT, myBoard.getCell(new Coordinate(1, 1)));
}
There are some new parts here. First is the annotation PrepareForTest. With this, we are telling PowerMock (which we are using for mocking) the class that we are about to test and the class that we are going to mock.
In this case, we are going to mock the behavior of Math.random. This is a static method, so we need to start off by telling PowerMock to mockStatic the class. Then, we simply choose a return value that we would like to be returned when Math.random is called. In this case, 0.5 will provide the index at the middle of the array. This means that the test can check that O is placed in the middle cell.
And there you have it!
So there’s our brief intro to mocking. Try it out yourself! As always, the full code is available on GitLab. Next, check out Chapter 5: How to find the time and motivation to unit test.