One Interface. Several Implementations. And only one Test? Yes!

Maybe others already use this testing-technique or pattern (Hey, we could call it OISI-test pattern ;-)). But I would like to share this information with you.

It is again my timefinder project where I had this kind of problem. I developed different implementations of the assignment algorithm for my heuristic ‘NoCollisionPrinciple’ which optimizes a timetable for universities. See track2 of the international timetabling competition for more information on that subject.

I created these implementations to compare the quality and performance of the heuristic which uses only the AssignmentAlgorithm interface.

While developing a new implementation the main question for me was:

Should I really write all the test code again?

The answer is simple: Nope!

I put all the test code in one abstract test class called AbstractAssignmentAlgorithmTester which uses only the interface in the normal testing methods (see the additional comments). Later I added the following method:

protected abstract AssignmentAlgorithm createAlgorithm();

Then I created an almost empty subclass which extends AbstractAssignmentAlgorithmTester for every new implementation. For example this one:

public class KuhnMunkresAlgorithmTest extends AbstractAssignmentAlgorithmTester {
    @Override
    protected AssignmentAlgorithm createAlgorithm() {
        return new KuhnMunkresAlgorithm();
    }
}

Now you can run every single test class which extends the abstract class. In NetBeans you can do this by going to the class and pressing SHIFT+F6. Or simply press ALT+F6 to run all tests.

Another problem I had to solve was the case of a faster approximation algorithm. The quality of the results were not always optimal and so certain tests will fail, even if the algorithm was correctly implemented. The solution is simple: overwrite these failing methods with stub methods to exclude them from the run. For example add the following method to TooSimpleApproxTest:

@Override
public void testCalculate() { /* we will get 6.0 instead of the best minimal total sum = 1.0 */ }
Additional comments:
  • The reason for the ‘strange’ name (…Tester) of the abstract class is that junit shouldn’t execute the abstract class (which would result in an error if we name it …Test). The testing tools testng and junit will hopefully make it easier in the future to test against interfaces and not only implementations. They could support testing abstract classes by looking for sub-test-classes and running all of them instead of the abstract class (and printing an error).
  • You can grab the source (Apache2 licensed) via:
    svn checkout https://timefinder.svn.sourceforge.net/svnroot/timefinder/trunk timefinder
    Look in the package timefinder-core/src/main/java/de/timefinder/core/algo/assignment

    To build it you have to use maven.
    And at the moment it is necessary to check out the latest revision of mydoggy (hopefully 1.5.0 will be released soon).

  • One example for a method in the abstract test class is the following:
    @Test
    public void testAssignmentCalculation() {
     // look in the following matrix for that specific assignment
     // which has the minimal sum. which should be 15.
     float matrix[][] = new float[][]{
       {4, 6, 1, 2},
       {5, 6, 7, 6},
       {7, 4, 5, 8},
       {4, 4, 6, 8}
     };
     // One of the best results is the following assignment:
     // Use the '4' in row_3 (room) and colum_0 (event)
     // Use the '4' in row_2 and colum_1
     // Use the '1' in row_0 and colum_2
     // Use the '6' in row_1 and colum_3
     // Do you find a better one ;-)
     int expResult[][] = new int[][]{{3, 0}, {2, 1}, {0, 2}, {1, 3}};
     int result[][] = algorithm.computeAssignments(matrix);
    
     // resSum should be 15. this is the sum over the assignment entries
     float resSum = AssignmentHelper.calculateSum(matrix, result);
    
     // resSum should be the same as the expected sum:
     float expSum = AssignmentHelper.calculateSum(matrix, expResult);
    
     assertEquals(expSum, resSum);
    }