JUnit - the Difference between Practice and @Theory

April 11, 2013 | 3 min Read

Lately a colleague showed me how to improve JUnit tests written for a distance calculator. Speaking with other developers I found out that the majority wasn’t aware of the undocumented @Theories Runner which can be found in an experimental package in JUnit, so I decided to share this valuable “experiment”.

In contrast to the parameterized JUnit test, the Theories-runner will try out all possible combinations of data points against your test methods marked with the @Theory annotation.

But let’s get your hands dirty. Obviously the first thing to do is run the test with the Theories-runner and declare some data points:

@RunWith(Theories.class)
public class DistanceCalculatorTest {

  @DataPoint
  static public LatLng es_1 = new LatLng( 49.0060285, 8.4006111 );
  @DataPoint
  static public LatLng es_2 = new LatLng( 49.0060285, 8.4005789 );
  @DataPoint
  static public LatLng es_3 = new LatLng( 49.0060056, 8.4005611 );
...

The implementation of the distance calculator should be:

  • Commutative,
  • Positive semidefinite
  • And fullfill the triangle equality

Which can be written down using the following three @Theory annotated test methods (which do not accept null values):

@Theory(nullsAccepted = false)
public void distanceIsCommutative(LatLng p1, LatLng p2) {
    assertThat(distanceCalculator.calculate(p1, p2), is(distanceCalculator.calculate(p2, p1)));
}

@Theory(nullsAccepted = false)
public void distanceIsPositiveSemidefinite(LatLng p1, LatLng p2) {
    assertThat(distanceCalculator.calculate(p1, p2), is(greaterThanOrEqualTo(0.)));
}

@Theory(nullsAccepted = false)
public void distanceFulfillsTriangleEquality(LatLng p1, LatLng p2, LatLng p3) {
    assertThat(distanceCalculator.calculate(p1, p2) + distanceCalculator.calculate(p2, p3), 
        is(greaterThanOrEqualTo(distanceCalculator.calculate(p1, p3) - delta)));
}

Sometimes you have to assume preconditions to test specific parts of your implementation. In the following @Theory the assumption is either p1 or p2 is null, which should lead to an IllegalArgumentException when using the calculator…

@DataPoint
public static LatLng nullPoint = null;

@Rule
public ExpectedException distanceCalculatorException = ExpectedException.none();

@Theory
public void distanceToNullNotDefined(LatLng p1, LatLng p2) throws Exception {
    assumeTrue(p1 == null || p2 == null);
    distanceCalculatorException.expect(IllegalArgumentException.class);
    distanceCalculator.calculate(p1, p2);
}

It is very simple to add more corner cases. The following snippet adds two representations of the north pole:

@DataPoint
static public LatLng north_pole_1 = new LatLng( 90, 10 );
@DataPoint
static public LatLng north_pole_2 = new LatLng( 90, 20 );

If you add a test which won’t work with the whole data points available (e.g. near the poles in this example) they can be easily filtered out by adding a more complex assumption at the beginning of the test method.

@Theory(nullsAccepted = false)
public void runningTowardsThePole(LatLng pt) {

    // this test does not work at the poles
    assumeThat(pt.lat, is(allOf(greaterThan(-89.), lessThan(89.))));
...
}

Just a side note. Rewriting the tests for the distance calculator in fact discovered a bug in the original implementation of the distance calculator using LatLng(-1, -1) as NIRVANA. Thanks again Hauke for teaching me this lesson.

Have fun verifying your code with theories.