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.