How to write a Unit Test in Java

Unit tests are an integral part of quality software development. It not only helps you check your work and prevent bugs, it's also a code design assistant and a form of documentation.

These days, automated testing is a requirement on just about every software development project. When you join a company, it's just expected that you know how to write good unit tests for your code.

In this post, we'll get comfortable with the concepts and basics of unit testing by first introducing a simple program and writing some unit tests for it.

Then we'll discover why some of it is hard to test, and make the appropriate changes so those bits can be unit tested as well.

  1. What is a Unit Test?
  2. Tools of the trade
  3. The Example
  4. Testing the main class
  5. Conclusion

But first, what is a unit test?

What is a Unit Test?

A Unit Test is the most fine-grained test that you can write. For a programming language like Java, a unit test directly exercises and verifies a single class.

Even though most classes interact with other classes in some way, a unit test is meant to isolate the class it's testing and replace the collaborating classes with fake or test implementations. We'll go over this in the example.

Unit tests can catch bugs in the code it's testing. It makes sure that given the right input, the code will take the right actions. However, it doesn't validate that other classes call it with the right input. That's for other types of tests like integration or end-to-end tests, covered in the previous post "why we test".

Unit tests also help you design your system well. If a class is hard to test, it's usually a sign that your design needs to be split into smaller, simpler, well-defined parts.

Finally, once your code and tests are complete, the tests provide a form of executable documentation for future developers to read.

Tools of the trade

In Java, the main library we're going to use for testing is JUnit 4. In fact, if you're using Intellij, Ecliipse, or NetBeans, JUnit testing should be built-in. If you have trouble setting up a project for unit testing, send me an email and I'll help you out!

The Example

I'll introduce some code that we can test, and then we'll write a few unit tests for it. The following code will try to guess your age. It will guess an age, and then you say "higher" if it guessed too low, or "lower" if it guessed too high. You say "correct" if it gets your age exactly right.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// file src/main/java/com/dacklabs/ageguesser/Main.java
package com.dacklabs.ageguesser;

import java.util.Scanner;

import static com.dacklabs.ageguesser.HintDirection.HIGHER;
import static com.dacklabs.ageguesser.HintDirection.LOWER;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("I will guess your age, and you tell me if it was higher, lower, or correct.");
        AgeGuesser guesser = new AgeGuesser();
        boolean correct = false;
        while (!correct) {
            int guess = guesser.guess();
            System.out.println(String.format("Are you %d years old? (possible answers: higher, lower, correct)", guess));
            String answer = scanner.nextLine().toLowerCase();

            if (answer.contains("higher")) {
                guesser.addHint(HIGHER);
            } else if (answer.contains("lower")) {
                guesser.addHint(LOWER);
            } else if (answer.contains("correct")) {
                correct = true;
            } else {
                System.out.println(String.format("I didn't understand \"%s\". Try again", answer));
            }
        }
        System.out.println(String.format("Hooray! I only needed %d hints!", guesser.hintsReceived()));
    }
}

// file src/main/java/com/dacklabs/ageguesser/AgeGuesser.java
package com.dacklabs.ageguesser;

import static com.dacklabs.ageguesser.HintDirection.HIGHER;

public class AgeGuesser {

    private int lowerBound = 0;
    private int upperBound = 100;
    private int hints = 0;

    public int guess() {
        int range = upperBound - lowerBound;
        return range / 2 + lowerBound;
    }

    public void addHint(HintDirection direction) {
        hints++;
        int lastGuess = guess();
        if (HIGHER.equals(direction)) {
            lowerBound = lastGuess;
        } else {
            upperBound = lastGuess;
        }
    }

    public int hintsReceived() {
        return hints;
    }
}

// file src/main/java/com/dacklabs/ageguesser/HintDirection.java
package com.dacklabs.ageguesser;

public enum HintDirection {
    HIGHER, LOWER
}

I created a github repo with this code - you can see the commit here or clone and run it.

You can run the above code in your favorite IDE and interact with it - here's a sample interaction:

I will guess your age, and you tell me if it was higher, lower, or correct. Are you 50 years old? (possible answers: higher, lower, correct) higher Are you 75 years old? (possible answers: higher, lower, correct) higher Are you 87 years old? (possible answers: higher, lower, correct) lower Are you 81 years old? (possible answers: higher, lower, correct) correct Hooray! I only needed 3 hints!

Your First Test

Alright, now that you've played around with that for a bit, let's write a unit test for the AgeGuesser class, since it's self-contained.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserTest.java
package com.dacklabs.ageguesser;

import org.junit.Test;

import static com.dacklabs.ageguesser.HintDirection.HIGHER;
import static com.dacklabs.ageguesser.HintDirection.LOWER;
import static org.junit.Assert.*;

public class AgeGuesserTest {

    @Test
    public void ageGuesserAlwaysStartsAt50() {
        AgeGuesser guesser = new AgeGuesser();

        int guess = guesser.guess();

        assertEquals("Guess was not 50!", 50, guess);
    }
}

If you're running Intellij, you should be able to right-click on this file and click on Run ageGuesserAlwaysStartsAt50() to run the test. It should pass!

Intellij Test Passed

Let's go through it line-by-line

The @Test on line 3 is a JUnit annotation saying that this method is a test, and it should run.

Line 4 is creating the test name. Your name should basically describe what the test is trying to verify. In this case, we want to make sure that the guesser always starts at 50.

Line 5 creates our SUT, or "Subject Under Test". This is the thing we're testing. We create a fresh one in this test, so we can run methods on it. This phase of a unit test is called "the setup" because it's where we initialize the starting state for our scenario.

Line 7 is where we actually exercise our class, i.e. make it do the thing we're going to test. In this case, we just call guess() so it gives the first guess it has.

Finally, on line 9 we do what's called an "assertion". This is the part of the test that checks various parts of the result, and creates errors if they don't match our expectations.

There are many ways to assert things, but assertEquals is the simplest. The first parameter is a message that should be output if the test fails. This isn't required, but sometimes helps explain the context around why something might fail. The second parameter is the expected value (50 in this case). And the third parameter is the actual value we got when we ran our SUT.

Rounding out the AgeGuesser class

Let's write another test to verify that multiple hints can be given and have it adjust its guess.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserTest.java
@Test
public void ageGuesserHalfsEachGuessAfterLowerHint() {
    AgeGuesser guesser = new AgeGuesser();

    guesser.addHint(LOWER);
    assertEquals(25, guesser.guess());

    guesser.addHint(LOWER);
    assertEquals(12, guesser.guess());

    guesser.addHint(LOWER);
    assertEquals(6, guesser.guess());
}

This one calls addHint multiple times, and verifies that the guess is halved each time a hint is given to guess "lower".

There is a tradeoff here in writing this test - we are locking in the particular algorithm that the AgeGuesser class uses. If we were to change how the AgeGuesser class does it's calculation, then we'd have to update this test.

In this case, I think it's worth it to create the test to verify that the dividing and adding we did in the implementation does what we intended.

Last we have a test that verifies that the class will keep track of the number of hints it recieved.

1
2
3
4
5
6
7
8
9
10
11
12
13
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserTest.java
@Test
public void ageGuesserCountsTheNumberOfHintsItGets() {
    AgeGuesser guesser = new AgeGuesser();

    assertEquals(0, guesser.hintsReceived());

    guesser.addHint(LOWER);
    assertEquals(1, guesser.hintsReceived());

    guesser.addHint(HIGHER);
    assertEquals(2, guesser.hintsReceived());
}

In both this test and the one above it, you can see we have multiple assertions in the test.

Some people say you should keep to a strict "one assertion per test" philosophy. The reasoning behind it is that you don't want your test to be too complicated. If you have too much code in a test (and assert too many different things), it can be hard to tell what's going on.

In this case, however, we are verifying one aspect of the class, but have multiple assertions along the way to make sure it does precisely what we expect; if we only had one assertion at the end, we couldn't be sure that the previous operations were also correct. This is the right way to have multiple assertions.

Testing the Main class

So we've written tests for the AgeGuesser class, but you may have noticed we completely left out the Main class - the one that gets user input, translates it into an enum, and outputs the guesses to the user.

It's not tested at all!

In it's current state, it's hard to test the Main class because it takes external inputs (i.e. standard IO). You don't want to run your unit tests and have it prompt you for some input 30 times!

Refactoring Main to be more testable

When we encounter something that is hard to test, we have two options: we can determine the code in question doesn't have enough interesting logic to be worth the effort, or we can split the code into a testable part and a non-testable part.

There's too much logic here to ignore, so we're going to do the latter - split the code into parts so the logic can be tested.

Here's the refactored code that's easier to test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// src/main/java/com/dacklabs/ageguesser/Main.java
package com.dacklabs.ageguesser;

public class Main {
    public static void main(String[] args) {
        AgeGuesserUI ui = new AgeGuesserUI(new SysIO(), new AgeGuesser());
        ui.start();
    }
}

// src/main/java/com/dacklabs/ageguesser/ConsoleIO.java
package com.dacklabs.ageguesser;

public interface ConsoleIO {
    String getUserInput();

    void printLine(String line);
}

// src/main/java/com/dacklabs/ageguesser/AgeGuesserUI.java
package com.dacklabs.ageguesser;

public final class AgeGuesserUI {
    private final ConsoleIO io;
    private final AgeGuesser guesser;

    public AgeGuesserUI(ConsoleIO io, AgeGuesser guesser) {
        this.io = io;
        this.guesser = guesser;
    }

    public void start() {
        io.printLine("I will guess your age, and you tell me if it was higher, lower, or correct.");
        boolean correct = false;
        while (!correct) {
            int guess = guesser.guess();
            io.printLine(String.format("Are you %d years old? (possible answers: higher, lower, correct)", guess));
            String answer = io.getUserInput().toLowerCase();

            if (answer.contains("higher")) {
                guesser.addHint(HintDirection.HIGHER);
            } else if (answer.contains("lower")) {
                guesser.addHint(HintDirection.LOWER);
            } else if (answer.contains("correct")) {
                correct = true;
            } else {
                io.printLine(String.format("I didn't understand \"%s\". Try again", answer));
            }
        }
        io.printLine(String.format("Hooray! I only needed %d hints!", guesser.hintsReceived()));
    }
}

// src/main/java/com/dacklabs/ageguesser/SysIO.java
package com.dacklabs.ageguesser;

import java.util.Scanner;

public final class SysIO implements ConsoleIO {

    private final Scanner scanner = new Scanner(System.in);

    @Override
    public String getUserInput() {
        return scanner.nextLine();
    }

    @Override
    public void printLine(String line) {
        System.out.println(line);
    }
}

You can see the github diff as well for a little more clarity.

As you can see, Main is now only two lines of code! We took nearly all the logic out of the class, so there's really nothing needed to test there anymore.

Of course, we gained several new classes - the main one being AgeGuesserUI. However, notice that AgeGuesserUI doesn't directly make calls to a Scanner(System.in) or System.out.println!

Instead, we have an interface now called ConsoleIO which represents the two calls we need to make (getting a line of user input, and printing lines of text). And then we created SysIO as a concrete class that implements the interface, running the Scanner and System.out code that was running before.

You'll see why having this functionality split into classes like this will greatly help us unit test them.

Using dependency injection

Take extra note to how AgeGuesserUI gets access to ConsoleIO and AgeGuesser classes. Pre-created instances of both are being passed into the constructor of AgeGuesserUI (see line 21 in the code snippet).

What this means is that AgeGuesserUI is not concerned with how to create a ConsoleIO or AgeGuesser - the calling class (Main, in this case) will create them for it.

The name for passing other classes into the constructor of a class is dependency injection. Both ConsoleIO and AgeGuesser are dependencies of AgeGuesserUI, and the act of passing them in the constructor is the injection part.

This is great because we can now choose to pass in different implementations of those dependencies during test time, and we're going to do just that!

Testing Main using Stubs

Now that we have refactored out code to use dependency injection, let's write some tests for the AgeGuesserUI.

We aren't going to use the same implementation of ConsoleIO that we used in our Main class. Instead we'll use a stub.

A stub is a class that you create specifically for testing, which is used instead of one of your real dependencies. It's an alternate implementation that allows you to inject test inputs into your SUT (subject under test) and verify the outputs after the fact.

For instance, we're going to create a stub for ConsoleIO that doesn't do the real console operations. Instead, we're going to create a version that returns a hardcoded list of user inputs, and collects a list of created outputs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// file src/test/java/com/dacklabs/ageguesser/FakeIO.java
package com.dacklabs.ageguesser;

public final class FakeIO implements ConsoleIO {

    private int lineIndex = 0;
    private final String[] lines;

    // This is public so the test can inspect it easily
    public final List<String> output = new ArrayList<>();

    // lines are what will pretend to be coming from System.in
    public FakeIO(String... lines) {
        this.lines = lines;
    }

    @Override
    public String getUserInput() {
        // return the next line that was passed in
        return lines[lineIndex++];
    }

    @Override
    public void printLine(String line) {
        // the SUT calls this, and we just record it for later inspection
        output.add(line);
    }
}

This way, we can use it instead of the real SysIO class in our tests, and then control what user input gets sent to the program, and interrogate the output as well.

Let's write a test that verifies the smallest interaction - where the user just accepts the first guess.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserUITest.java
package com.dacklabs.ageguesser;

import org.junit.Test;

import static org.junit.Assert.*;

public class AgeGuesserUITest {
    @Test
    public void uiGuessesAgeAndCelebratesIfCorrect() {
        // System.in will just be "correct"
        FakeIO io = new FakeIO("correct");
        AgeGuesser guesser = new AgeGuesser();

        AgeGuesserUI ui = new AgeGuesserUI(io, guesser);
        ui.start();

        assertEquals(3, io.output.size());

        String expectedFirstLine = "I will guess your age";
        String expectedSecondLine = String.format("Are you %d years old? ", guesser.guess());
        String expectedThirdLine = "Hooray! I only needed 0 hints";

        assertTrue("first message wasn't instructions",
                io.output.get(0).contains(expectedFirstLine));

        assertTrue("second message doesn't contain the guess",
                io.output.get(1).contains(expectedSecondLine));

        assertTrue("third message doesn't contain a count of guesses",
                io.output.get(2).contains(expectedThirdLine));
    }
}

This test creates the AgeGuesserUI with a FakeIO and a real AgeGuesser and passes them into the constructor. It then calls start() which runs through the interaction using the inputs that were passed in on line 3 (in this case, "correct" to immediately accept the first guess).

After we call start(), the output list inside of FakeIO is now filled with whatever the console output would have been. We expect that it's 3 lines - one for the instructions, one for the guess, and a third listing the number of hints it got.

We created one assertion for each of these lines, and we put in error messages to help with debugging if one of them fails.

Next, we'll test that the program will keep guessing until it's correct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserUITest.java
@Test
public void uiKeepsGuessingAgeUntilCorrect() {
    FakeIO io = new FakeIO("higher", "lower", "correct");

    AgeGuesserUI ui = new AgeGuesserUI(io, new AgeGuesser());
    ui.start();

    assertEquals(5, io.output.size());

    String ageQuestionRegex = "Are you \\d+ years old.*";

    assertTrue(io.output.get(1).matches(ageQuestionRegex));
    assertTrue(io.output.get(2).matches(ageQuestionRegex));
    assertTrue(io.output.get(3).matches(ageQuestionRegex));
}

Above, we've created another FakeIO, this time with 3 inputs - "higher", "lower", and "correct". This should cause the ui to invoke the guesser 3 times.

Once again, we assert the number of lines (3 guesses plus intro and last). This time, we only assert that the "age question" was asked 3 times.

The reason we didn't assert everything is because we already tested the other lines in a previous test, and don't want to be redundant. It's natural to have a little redundancy in unit tests, but we want to avoid it when we can.

Lastly, we'll test some invalid input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// file src/test/java/com/dacklabs/ageguesser/AgeGuesserUITest.java
@Test
public void uiTellsUserWhenItGetsAnInvalidCommandAndContinues() {
    FakeIO io = new FakeIO("aljsfl;kasjdf", "correct");

    AgeGuesserUI ui = new AgeGuesserUI(io, new AgeGuesser());
    ui.start();

    assertEquals(5, io.output.size());

    String secondLine = io.output.get(2);
    String expectedSecondLine = "I didn't understand \"aljsfl;kasjdf\"";
    String lastLine = io.output.get(4);

    assertTrue("Expected invalid command message but got " + secondLine, secondLine.contains(expectedSecondLine));
    assertTrue("Expected Hooray but got " + lastLine, lastLine.contains("Hooray!"));
}

Here, we give FakeIO a line of gibberish, run the AgeGuesserUI, and verify that it says it didn't understand the command. We also verify that it then asks for the age again.

As with any example, it's a little contrived and there are other ways it could have been tested (for instance, with mocks we haven't discussed yet), but it's a good start.

Conclusion

Alright, that's a lot of ground to cover for one post! If you haven't already, clone the example code on github and try adding your own tests or functionality. Let me know at contact@fullstack.industries if you have any questions or comments!