Using JUnit 4 Theories to Test Contracts 5

Posted by Berin Loritsch Tue, 09 Feb 2010 16:08:00 GMT

I’ve been putting off upgrading to JUnit 4 for a while. After all, just how much does it really buy you? It turns out, that JUnit 4 grants you a number of advantages that weren’t available with JUnit 3. One of those features is currently in an experimental phase: Theories. Theories let you specify a bunch of data points, which can be applied to each theory in your class. For example, if you want to test some boundary conditions on a class, you can make it happen easily like this:

@RunWith(Theories.class)
public class VerifyMyAlgorithm {
    @DataPoint
    public static int limLow = 0;

    @DataPoint
    public static int limHigh = 3000;

    @DataPoint
    public static int belowLimLow = limLow - limHigh;

    @DataPoint
    public static int aboveLimHigh = limHigh * 2;

    @DataPoint
    public static int median = (limHigh - limLow) / 2;

    @Theory
    public void verifyTwoParameters(int first, int second) {
        assertInRange(limLow, limHigh, MyMath.algorithm(first,second));
    }
}

I wanted to keep it simple just so you can get the basic idea. Essentially the @RunWith() annotation changes the basic behavior of your test class. It enables the use of the following annotations: @DataPoint, @DataPoints (more on this later), and @Theory. The data points you specified are collected and matched together so that each unique combination of datapoints is applied to the parameters in your theory. If your theory takes one parameter, the theory is run once per data point. If your theory takes two parameters it is run with every unique combination (15 times in this case).

But wait, there’s more! Sometimes we want to generate our datapoints with code. For example, we may need prime numbers up two four significant digits, or we need to initialize our data. In order to do that, we can use the @DataPoints annotation. The return type of your static method will be an array of datapoints. I could rewrite the example above like this:

@RunWith(Theories.class)
public class VerifyMyAlgorithm {
    @DataPoints
    public static int[] attemptLimits() {
        return new int[] {0,3000,-3000,6000,1500};
    }

    @Theory
    public void verifyTwoParameters(int first, int second) {
        assertInRange(limLow, limHigh, MyMath.algorithm(first,second));
    }
}

The DataPoints functionality in concert with the theories are the foundation of what is ncessary to automatically test the contracts of each service. I’ve thrown together a rudimentary class scanner that uses reflection to iterate over classes in the classpath to determine if they match the criteria for the service. For example, you can look at all classes that implement an interface, or extend a base class, or even have an annotation. From that list of classes we can test the contracts of the implementations. The nice aspect of this approach over creating a base test class and extend it manually for each implementation we write, is that it automatically finds the new implementations for you. I have yet to release the class scanner, but you can implement your own pretty well. Here is an example of how it would be used:

@RunWith(Theories.class)
public class EnforceLocakableContracts {
    @DataPoints
    public static Lockable[] collectImplementations() {
        Collection<Class<?>> klasses = new ClassCollector()
                .assignableTo(Locakable.class).recurse().collect();

       Lockable[] implementations = new Lockable[klasses.size()];
       int i = 0;
       for (Class<?> klass : klasses) {
           implementation[i] = klass.newInstance();
           i++;
       }

       return implementations;
    }

    @Theory
    public void lockShouldApplyToOneUser(Lockable lockable) {
        User one = TestSupport.createUser("one");
        User two = TestSupport.createUser("two");

        assertFalse(lockable.isLocked());

        lockable.acquireLock(one);

        assertFalse(lockable.canAccess(two);
    }

    @Theory
    public void lockableShouldBeAccessibleByLocker(Lockable lockable) {
        User one = TestSupport.createUser("test");

        assertFalse(lockable.isLocked());

        lockable.acquerLock(one);

        assertTrue(lockable.canAccess(one);
    }
}

So on and so forth. Just add a new Theory for each aspect of the implementing class you want to test. With this approach, testing implmentations of an interface is essentially future proofinf yourself against lazy programmers. There are some aspects that this approach doesn’t handle well just yet, such as complex setup for each object.

As of JUnit 4.7 there are a few problems, but they are not insurmountable:

  • You see one pass/fail per theory (not per implementation)
  • Failures do not show what implementation failed (fixed in the stacktraces provided by JUnit 4.8.1)
  • The first failure kills future tests. You may have errors in multiple implementations but you won’t be able to find it until you fix the first implementation.

The best thing is to upgrade to JUnit 4.8.1, which is still not available in Maven repositories yet. However, you can also use the System.out approach to find out what the last thing tested was.

There is a feature request for theories to allow you to run them discretely. I.e. each run gets displayed in your test report individually with the parameters supplied in the test name. That will address the shortcomings and make this an even better approach.

Comments

Leave a response

  1. Steve about 2 hours later:

    Theories seem to overlap severely with parameterized tests. For example, your Lockable example can already be written with the parameterized runner, so what’s the point of theories?

  2. Berin about 21 hours later:

    Perhaps there is a fair amount of overlap in the way I am using them. Some of that stems from my limited knowledge of JUnit 4. After looking up parameterized tests, they look to be a pain to set up. That little bit aside, there is one thing that Theories do that parameterized tests don’t do:

    They apply every data point in every combination automatically. As long as your theory takes multiple parameters, you can mix and match to your hearts content. While that gives you less control, it does help you build a large control set quickly.

  3. Mike about 23 hours later:

    Informative article. Thanks!!!

    Would help if you ran the code you wrote. In first example it will @Theory 25 times.

  4. Berin 1 day later:

    Your kind-hearted rebuke is accepted. I was essentially distilling some of the work I did on another computer in this article. I’m working on addressing my little complaints, which has yet to be tested.

  5. Berin 3 days later:

    I’ve found a few more resources on Theories, which it appears that I’m using them as intended:

    http://shareandenjoy.saff.net/tdd-specifications.pdf http://groups.csail.mit.edu/pag/pubs/test-theory-demo-oopsla2007.pdf

    Although there were some fundamental things that were not clear. It will take some time to form a proper reaction to this though. They are highly recommended reads.

Comments