Pragmatic Unit Testing in Java: Invariants

by Jeff Langr

January 21, 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 at least one 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, and that these resurrected examples will be useful to at least someone.

Invariants

Excessive use of primitive datatypes is a code smell known as primitive obsession. A benefit of an object-oriented language like Java is that it lets you define your own custom abstractions in the form of classes.

A circle has only 360 degrees. Rather than store the direction of travel as a native type like an int, you can create a class named Bearing that encapsulates the direction along with logic to constrain its range. Tests show how it works.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ABearing {
   @Test
   void throwsWhenValueTooLarge() {
      assertThrows(BearingOutOfRangeException.class,
         () -> new Bearing(Bearing.MAX + 1));
   }

   @Test
   void answersValidBearing() {
      assertEquals(Bearing.MAX, new Bearing(Bearing.MAX).value());
   }

   @Nested
   class AngleTo {
      @Test
      void isDifferenceFromOtherBearingValue() {
         assertEquals(3, new Bearing(15).angleTo(new Bearing(12)));
      }

      @Test
      void isNegativeWhenThisBearingSmaller() {
         assertEquals(-3, new Bearing(12).angleTo(new Bearing(15)));
      }
   }
}

The constraint is implemented in the constructor of the Bearing class:

public record Bearing(int value) {
   public static final int MAX = 359;

   public Bearing {
      if (value < 0 || value > MAX)
         throw new BearingOutOfRangeException();
   }

   public int angleTo(Bearing bearing) {
      return value - bearing.value;
   }
}

Note that angleTo() returns an int. You’re not placing any range restrictions (such as that it must not be negative) on the result.

The Bearing abstraction makes it impossible for client code to create out-of-range bearings. As long as the rest of the system accepts and works with Bearing objects, the gate on range-related defects is shut.

Other constraints might not be as straightforward. Suppose you have a class that maintains two points, each an x, y integer tuple. The constraint on the range is that the two points must describe a rectangle with no side greater than 100 units. That is, the allowed range of values for both x, y pairs is interdependent.

You want a range assertion for any behavior that can affect a coordinate, to ensure that the resulting range of the x, y pairs remains legitimate—that the invariant on the Rectangle holds true.

More formally: An invariant is a condition that holds true throughout the execution of some chunk of code. In this case, you want the invariant to hold true any time its state changes during the lifetime of the Rectangle object.

You can add invariants in the form of assertions to the @AfterEach method so that they are verified upon completion of each test. Here’s what an implementation for the invariant for the constrained Rectangle class looks like:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import util.ExpectToFail;

import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ARectangle {
   private Rectangle rectangle;

   @AfterEach
   void ensureInvariant() {
      assertTrue(constrainsSidesTo(rectangle, 100),
         format("rectangle invariant: neither sides can exceed 100"));
   }

   boolean constrainsSidesTo(Rectangle rect, int length) {
      return
         Math.abs(rect.origin().x() - rect.opposite().x()) <= length &&
            Math.abs(rect.origin().y() - rect.opposite().y()) <= length;
   }

   @Test
   void answersArea() {
      rectangle = new Rectangle(new Point(5, 5), new Point(15, 10));
      assertEquals(50, rectangle.area());
   }

   @Test
   void allowsDynamicallyChangingSize() {
      rectangle = new Rectangle(new Point(5, 5));
      rectangle.setOppositeCorner(new Point(125, 55));
      assertEquals(6000, rectangle.area());
   }
}

Assuming the answersArea test runs first (which may or may not be true):

  • After JUnit runs the answersArea test, it runs the @AfterEach method, which passes because the rectangle created has the size 10x5.

  • After JUnit runs the allowsDynamicallyChangingSize test, it runs the @AfterEach method. Its assert fails because rectangle has a side with length 120, which violates the invariant.

Knowing that JUnit will always check the invariant (for all your tests that manipulate a rectangle instance) should help you sleep safely.


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.