Unit testing is really important for code quality (and for making your life easier when hunting for bugs), but if you’ve never done it before, where do you begin? To help out, we’re sharing the Definitive Guide to Unit Testing in Java. Throughout the course of the tutorial, the complexity of the code will increase, but the tutorials will focus on the unit testing that is used.
The topics we will cover in this guide are:
- Intro: What are the different types of tests?
- Chapter 1: How to write your first unit test
- Chapter 2: How to measure code coverage
- Chapter 3: How to build a complete test suite
- Chapter 4: Mocking in unit tests
- Chapter 5: Finding the time and motivation to unit test
- Chapter 6: Unit testing mistakes to avoid
- Chapter 7: How automated unit tests speed up continuous integration
- Chapter 8: How to deliver on the promises of DevOps
- Chapter 9: Why imperfect tests are better than no tests
Let’s Begin
Let’s use a web-based Java TicTacToe game to demonstrate how to write your first unit test. First, here’s the code that checks to see if anyone has won the game.
public Player whoHasWon() {
ArrayList<Coordinate[]> winningPositions = new ArrayList<>();
// rows
winningPositions.add(new Coordinate[] {new Coordinate(0,0), new Coordinate(1,0), new Coordinate(2,0)});
winningPositions.add(new Coordinate[] {new Coordinate(0,1), new Coordinate(1,1), new Coordinate(2,1)});
winningPositions.add(new Coordinate[] {new Coordinate(0,2), new Coordinate(1,2), new Coordinate(2,2)});
// columns
winningPositions.add(new Coordinate[] {new Coordinate(0,0), new Coordinate(0,1), new Coordinate(0,2)});
winningPositions.add(new Coordinate[] {new Coordinate(1,0), new Coordinate(1,1), new Coordinate(1,2)});
winningPositions.add(new Coordinate[] {new Coordinate(2,0), new Coordinate(2,1), new Coordinate(2,2)});
// diagonals
winningPositions.add(new Coordinate[] {new Coordinate(0,0), new Coordinate(1,1), new Coordinate(2,2)});
winningPositions.add(new Coordinate[] {new Coordinate(2,0), new Coordinate(1,1), new Coordinate(0,2)});
for (Coordinate[] winningPosition : winningPositions) {
if (getCell(winningPosition[0]) == getCell(winningPosition[1])
&& getCell(winningPosition[1]) == getCell(winningPosition[2])) {
if (getCell(winningPosition[0]) != null) {
return getCell(winningPosition[0]);
}
}
}
return null;
}
Next, we should write some unit tests to ensure the behavior is correct and doesn’t break in the future. So where do you start?
We will need to introduce a test framework into the project. For this example, we will use JUnit. To do this in Maven, simply add the following to your dependencies in the pom.xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
Next, it’s time to create a class for the unit tests to exist in. We want the tests to be in the same package as the class that we are testing. In our example, this is package com.diffblue.javademo.tictactoe;
Which means that the file should be in com/diffblue/javademo/tictactoe.
For a Maven project, this needs to be prefixed with src/test/java
Therefore, the path from the project root is src/test/java/com/diffblue/javademo/tictactoe.
This means that all the test classes are separated nicely from the main source code.
Finally, the class should follow the format
Test which, for this example, means the class will be BoardTest and the file obviously BoardTest.java
Note: when running tests with Maven, the default is to search for classes that are suffixed with Test.
With all that in mind, create a class:
package com.diffblue.javademo.tictactoe;
public class BoardTest {
}
For this example, let’s create a test to check that detecting a player winning through the top row works correctly.Create a method for this test:
package com.diffblue.javademo.tictactoe;
import org.junit.Test;
public class BoardTest {
@Test
public void playerOTopRow() {
// Arrange
// Act
// Assert
}
}
This is slightly different to the methods that you have written in your source code. Working through the method: First there is an annotation to say that this is a test:
@Test
Also, note the relevant import. This will tell the tools that this method is a test. If you are using IntelliJ or Eclipse, you will see that you can now run this method as a test.
This is a good time to point out that unit tests are designed to protect against future mistakes. This means that your test needs to be easy to read and understand both by other developers and your future self. To split up tests to aid in readability, let’s add three comments: arrange, act and assert.
Now to start building out the test!
We need to set up an environment where the top row of the board is Naughts:
package com.diffblue.javademo.tictactoe;
import org.junit.Test;
public class BoardTest {
@Test
public void playerOTopRow() {
// Arrange
Board myBoard = new Board();
myBoard.setCell(new Coordinate(0,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(0,1), Player.CROSS);
myBoard.setCell(new Coordinate(1,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(1,1), Player.CROSS);
myBoard.setCell(new Coordinate(2,0), Player.NOUGHT);
// Act
// Assert
}
}
Now we have a board that has Naughts winning through the top row. Next, call the method whoHasWon() and collect the result.
package com.diffblue.javademo.tictactoe;
import org.junit.Test;
public class BoardTest {
@Test
public void playerOTopRow() {
// Arrange
Board myBoard = new Board();
myBoard.setCell(new Coordinate(0,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(0,1), Player.CROSS);
myBoard.setCell(new Coordinate(1,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(1,1), Player.CROSS);
myBoard.setCell(new Coordinate(2,0), Player.NOUGHT);
// Act
Player result = myBoard.whoHasWon();
// Assert
}
}
Now we have a test that sets up the environment and calls the method under test.
There is one final step to complete the test: add an Assert. The assert is the key part to the test, it is the thing that is checked to say whether the test has passed or failed. Here, we are checking that result is naught.
Here is our complete test:
package com.diffblue.javademo.tictactoe;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BoardTest {
@Test
public void playerOTopRow() {
// Arrange
Board myBoard = new Board();
myBoard.setCell(new Coordinate(0,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(0,1), Player.CROSS);
myBoard.setCell(new Coordinate(1,0), Player.NOUGHT);
myBoard.setCell(new Coordinate(1,1), Player.CROSS);
myBoard.setCell(new Coordinate(2,0), Player.NOUGHT);
// Act
Player result = myBoard.whoHasWon();
// Assert
assertEquals("Player O didn't win in the top row", Player.NOUGHT, result);
}
}
Looking at the assert, note the message (first argument to the assert). When a test fails, this message is printed in the results. This can give a clear indication of what has gone wrong to the person debugging the tests.
Another note: the order of the arguments is expected result first, and then the actual result. Because tools will include the expected and the actual results in the output, it is important to avoid confusion by getting these the correct way round.
Having finished the test, we can now run all the tests using mvn test and we will see that our test passes:
[INFO] ------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.diffblue.javademo.tictactoe.BoardTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s - in com.diffblue.javademo.tictactoe.BoardTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
The full source code for this tutorial is available here.
Congrats! With that, you’ve written your first test. Up next, check out Chapter 2: How to measure code coverage.