Experimenting with Theories

The very first 4.x release of JUnit contained support for custom test runners. Moreover, it came with the Parameterized test runner that allows to execute the test cases in a test class against a collection of values, i.e. parameters.

The example that comes with the Javadoc of the Parameterized class tests an imaginary Fibonacci calculator for a number of data points:

@RunWith(Parameterized.class)
public class FibonacciParameterizedTest {

    @Parameters
    public static List<Object[]> data() {
        return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 },
                { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } });
    }

    private final int input;
    private final int expected;

    public FibonacciParameterizedTest(int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void test() {
        assertEquals(expected, Fibonacci.compute(input));
    }
}

JUnit 4.4 introduced Theories. A theory is an abstraction of a concrete test scenario, i.e. while a test specifies the behavior in one particular case, a theory captures more than a single scenario but is usually not as detailed in its assertions.

When using a Parameterized test you usually specify the input along with the expected output. Of course, you can do the same with a theory. However, theories allow for a complete different approach to testing the Fibonacci calculator.

E.g. a simple theory could state that for one of the seeds, i.e. 0 or 1, the same number is returned as result. Another theory could test the recurrence relation, i.e. that Fibonacci(n) always equals Fibonacci(n-1) + Fibonacci(n-2). In Java this can be written as:

@RunWith(Theories.class)
public class FibonacciTheories {

    @DataPoints
    public static int[] VALUES = { 0, 1, 2, 3, 4, 5, 6 };

    @Theory
    public void seeds(int n) {
        assumeTrue(n <= 1);
        assertEquals(n, compute(n));
    }

    @Theory
    public void recurrence(int n) {
        assumeTrue(n > 1);
        assertEquals(compute(n - 1) + compute(n - 2), compute(n));
    }
}

Even shorter yet:

@RunWith(Theories.class)
public class FibonacciTheories {

    @Theory
    public void seeds(@TestedOn(ints = { 0, 1 }) int n) {
        assertEquals(n, compute(n));
    }

    @Theory
    public void recurrence(@TestedOn(ints = { 2, 3, 4, 5, 6 }) int n) {
        assertEquals(compute(n - 1) + compute(n - 2), compute(n));
    }
}

So, which test is better? Actually, I am still undecided. While theories certainly look more elegant, a parameterized tests states its assumptions more clearly. The answer seems to be, as always: It depends.