Writing Good Tests

A test should be easy to read, so that it states what the code is supposed to do. And when a test fails, it should tell you where the fault is. What follows are some guidelines for writing good tests.

Name the scenario, not the method

When a test fails, the first thing JUnit shows you is its name. Ideally, the name tells you what is broken, so you do not have to read the test code. Name a test after the scenario it checks, meaning the behavior it is checking, not after the method it calls:

@Test
public void newListIsEmpty() { ... }

@Test
public void addThenGetReturnsElement() { ... }

@Test
public void removeLastElementLeavesListEmpty() { ... }

Each of these reads like a sentence about the list. Compare them with the following names, which are less useful:

@Test
public void test1() { ... }      // tells you nothing

@Test
public void testAdd() { ... }    // what about add?

test1 is the least useful of these. Never number your tests. testAdd is better but still vague: a class may have five tests about add, each checking a different scenario.

One behavior per test

Each test should check one behavior. Do not write a single test that checks several behaviors:

@Test
public void checkAddAndRemove() {
  List<Integer> list = new ArrayList<>();
  list.add(10);
  list.add(20);
  assertEquals(2, list.size());
  list.remove(10);
  assertEquals(1, list.size());
  assertEquals(20, list.get(0));
}

When this fails with expected: <1> but was: <2>, what is broken? Maybe remove did nothing. Maybe add miscounted. Maybe remove deleted the wrong element. The test mixes several behaviors together, so when it fails it cannot tell you which one is broken. Split it into separate tests: addGrowsSize, removeDeletesElement, removeShrinksSize. Now each test checks one behavior, so when one of them fails you know which behavior is broken.

You cannot test in perfect isolation

Ideally, each test would check one method in perfect isolation. But in practice, tests often depend on other methods. A test checks a behavior, and observing a behavior usually takes several methods. For example, add has two effects: it grows the size, and it makes the element retrievable at the right index. To check the first, the test has to call size. To check the second, it has to call get. Before either can work, you have to construct a list, so both tests also depend on the constructor working. You cannot test add in perfect isolation.

What you can do is order your tests from the simplest to the most involved. Test the empty, freshly constructed list first, then a single add with size, then add plus get, and so on. If a basic operation is broken, the more involved tests that use it will fail as well. But the simplest test that uses it fails too, and that test uses so little of the class that it tells you where to look.

The @DisplayName annotation

JUnit gives us an annotation called @DisplayName that lets us attach a plain-English label to a test. That label appears in the test report, so you can write a sentence that says what the test checks.

import org.junit.jupiter.api.DisplayName;

@Test
@DisplayName("get() with an index past the end throws IndexOutOfBoundsException")
public void getPastEndThrows() { ... }

You should not use this annotation instead of a good method name. Give the test a good name, and use @DisplayName as an extra label that says more about the scenario.