Pragmatic Unit Testing in Java: Son of Invariants

by Jeff Langr

January 28, 2025

Pragmatic Unit Testing in Java has been published! In crafting the 3rd edition of this venerable classic, originally created by The pragmatic programmers Andy Hunt and Dave Thomas, I made significant cuts in order to keep the page count under 275 or so.

This article, and potentially more to come, represents one of those excised book segments, and you don’t need to read any of the book to follow along (as long as you’re familiar with JUnit basics).

I firmly believe good examples represent a core part of a solid book. May these resurrected examples be useful to you.

Son of Invariants

In my previous article on invariants, I demonstrated how you can use the @AfterEach hook to ensure that some condition always holds true after the execution of each test. In this article, you’ll see how you might also embed the ability to check invariants into the class you’re testing, so that your tests can remain independent of the implementation specifics.

A web app that displays a customer’s account history might require the customer to be logged in. The pop() method for a stack requires a nonempty stack. Shifting your car’s transmission from Drive to Park requires you to first stop the vehicle—if your transmission allowed the shift while the car was moving, it’d likely deliver some hefty damage to your fine Geo Metro.

When you make assumptions about any state, you should verify that your code is reasonably well-behaved when those assumptions are not met. Imagine you’re developing the code for your car’s microprocessor-controlled transmission. You want tests that demonstrate how the transmission behaves when the car is moving versus when it is not. Your tests for the Transmission code cover three critical scenarios: that it remains in Drive after accelerating, that it ignores the damaging shift to Park while in Drive, and that it does allow the shift to Park once the car isn’t moving:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static scratch.Gear.DRIVE;
import static scratch.Gear.PARK;

class ATransmission {
   private Car car = new Car();
   private Transmission transmission = new Transmission(car);

   @Test
   void remainsInDriveAfterAcceleration() {
      transmission.shift(DRIVE);

      car.accelerateTo(35);

      assertEquals(DRIVE, transmission.getGear());
   }

   @Test
   void ignoresShiftToParkWhileInDrive() {
      transmission.shift(DRIVE);
      car.accelerateTo(30);

      transmission.shift(PARK);

      assertEquals(DRIVE, transmission.getGear());
   }

   @Test
   void allowsShiftToParkWhenNotMoving() {
      transmission.shift(DRIVE);
      car.accelerateTo(30);
      car.brakeToStop();

      transmission.shift(PARK);

      assertEquals(PARK, transmission.getGear());
   }
}

The preconditions for a method represent the state things must be in for it to run. The precondition for putting a transmission in Park is that the car must be at a standstill. You want to ensure that the method behaves gracefully when its precondition isn’t met (in your case, ignore the Park request).

Postconditions state the conditions that you expect the code to make pass—essentially, the assert portion of your test. Sometimes this is simply the return value of a called method. You might also need to verify other side effects—changes to state that occur as a result of invoking behavior. In the allowsShiftToParkWhenNotMoving test case, calling brakeToStop() on the car instance has the side effect of setting the car’s speed to 0.

Invariants represent conditions that must always hold true. An example for the transmission: the current velocity cannot be greater than zero when the car is in Park.

Testing Ranges by Embedding Invariant Methods

The most common ranges you’ll test will likely depend on data-structure concerns, not application-domain constraints.

Take a look at a questionable implementation of a sparse array—a data structure designed to save space. The sweet spot for a sparse array is a broad range of indexes where most of the corresponding values are null. It accomplishes this goal by storing only non-null values, using a pair of arrays that work in concert: an array of indexes corresponds to an array of values.

Here’s the source for the SparseArray class:

import java.util.Arrays;
import java.util.Objects;

// fair warning to copy/paste folks: this code contains a defect!
public class SparseArray<T> {
   public static final int INITIAL_SIZE = 1000;
   private int[] keys = new int[INITIAL_SIZE];
   private Object[] values = new Object[INITIAL_SIZE];
   private int size = 0;

   public void put(int key, T value) {
      if (value == null) return;

      var index = binarySearch(key, keys, size);
      if (index != -1 && keys[index] == key)
         values[index] = value;
      else {
         insertAfter(key, value, index);
      }
   }

   public int size() {
      return size;
   }

   private void insertAfter(int key, T value, int index) {
      var newKeys = new int[INITIAL_SIZE];
      var newValues = new Object[INITIAL_SIZE];

      copyFromBefore(index, newKeys, newValues);

      var newIndex = index + 1;
      newKeys[newIndex] = key;
      newValues[newIndex] = value;

      if (size - newIndex != 0)
         copyFromAfter(index, newKeys, newValues);

      keys = newKeys;
      values = newValues;
   }

   private void copyFromAfter(int index, int[] newKeys, Object[] newValues) {
      var start = index + 1;
      System.arraycopy(keys, start, newKeys, start + 1, size - start);
      System.arraycopy(values, start, newValues, start + 1, size - start);
   }

   private void copyFromBefore(int index, int[] newKeys, Object[] newValues) {
      System.arraycopy(keys, 0, newKeys, 0, index + 1);
      System.arraycopy(values, 0, newValues, 0, index + 1);
   }

   @SuppressWarnings("unchecked")
   public T get(int key) {
      var index = binarySearch(key, keys, size);
      if (index != -1 && keys[index] == key)
         return (T) values[index];
      return null;
   }

   int binarySearch(int n, int[] nums, int size) {
      int low = 0;
      var high = size - 1;

      while (low <= high) {
         var midIndex = (low + high) / 2;
         if (n > nums[midIndex])
            low = midIndex + 1;
         else if (n < nums[midIndex])
            high = midIndex - 1;
         else
            return midIndex;
      }
      return low - 1;
   }
}

One of the tests you want to write involves ensuring that you can add a couple of entries, then retrieve them both:

public class ASparseArray {
   private SparseArray<Object> array;

   @BeforeEach
   public void create() {
      array = new SparseArray<>();
   }

// ... other tests ...

   @Test
   public void handlesInsertionInDescendingOrder() {
      array.put(7, "seven");
      array.put(6, "six");

      assertEquals("six", array.get(6));
      assertEquals("seven", array.get(7));
   }
}

The sparse-array code has some intricacies around tracking and altering the pair of arrays. One way to help prevent errors is to determine what invariants exist for the implementation specifics. In the case of the sparse-array implementation, which accepts only non-null values, the tracked size of the array must match the count of non-null values.

You might consider writing tests that probe at the values stored in the private arrays, but that would require exposing true private implementation details unnecessarily. Instead, you can devise a checkInvariants() method that can do the skullduggery for you, throwing an exception if any invariants (well, you have only one so far) fail to hold true.

public void checkInvariants() throws InvariantException {
  var nonNullValues = Arrays.stream(values).filter(Objects::nonNull).count();
  if (nonNullValues != size)
     throw new InvariantException("size " + size +
        " does not match value count of " + nonNullValues);
}

(You could also implement invariant failures using the Java assert keyword.)

You can scatter checkInvariants() calls in your tests any time you do something to the sparse-array object:

@Test
public void handlesInsertionInDescendingOrder() {
   array.put(7, "seven");
   array.checkInvariants();  // <--
   array.put(6, "six");
   array.checkInvariants();  // <--

   assertEquals("six", array.get(6));
   assertEquals("seven", array.get(7));
}

The test now errors out with an InvariantException:

util.InvariantException: size 0 does not match value count of 1
	at util.SparseArray.checkInvariants(SparseArray.java)
	at util.ASparseArray$StorageAndRetrieval.handlesInsertionInDescendingOrder(ASparseArray.java)
  ...

The code indeed has a problem with tracking the internal size. Challenge: where’s the defect?

(Don’t read this paragraph if you’d like to figure it out on your own.) HINT: You’ll need to increment size somewhere in the code. Remember that old joke about the consultant and the hammer? Know where to tap in this code, and you can call yourself an honorary consultant!

Even though the later parts of the test would fail anyway given the defect, the checkInvariants calls allow you to pinpoint more easily where the code is failing.

Custom index management code can exhibit a variety of potential errors. In other code involving indexes, you might consider checking invariants that protect against (potentially) bad states. Some such states that might be problematic:

  • Start and end index have the same value

  • First index is greater than last index

  • Index is negative

  • Index is greater than (size - 1)


Share your comment

Jeff Langr

About the Author

Jeff Langr has been building software for over 40 years and writing about it heavily for about 25. You can find out more about Jeff, learn from the many helpful articles and books he's written, or read one of his near-1500 combined blog, Agile in a Flash, and public posts.