Parameterized Tests in JUnit 4.x

Parameterized tests is one of the features that have come along in the JUnit 4.x releases that I don’t find myself using very often but when I do, I find tremendously useful. In a nutshell, parameterized tests allow you to run the same test over and over again using different values. I’ve found this approach very useful when you are testing a function and have a table of data defining the input value(s) and the expected result. That being said, parameterized tests can be used for testing more than just pure “functions.”

This post walks through a detailed example of using JUnit’s parameterized tests.

The Scenaio Under Test: Boxing the Compass

The example I’m going to use is that of turning a compass bearing in degrees into a directional compass point. I’m definitely not an expert in cartography or navigation techniques so if you happen to be one, please let me know if I’ve made any mistakes in the terminology.

Client code will want to provide a bearing in degrees and the number of directional points on the compass (see the Wikipedia articles on Boxing the Compass and Compass Rose if you are interested in a better description of this) and get back the compass point of that bearing. In other words, if you have a bearing of 65.23 degrees and an eight-point compass rose, that corresponds to a compass point of Northeast. In code, this looks like (using JUnit/Hamcrest):

assertThat(CompassPoint.fromBearing(65.32, CompassRose.EIGHT_POINT), is(CompassPoint.NE));
 
assertThat(CompassPoint.fromBearing(343, CompassRose.EIGHT_POINT), is(CompassPoint.N));
assertThat(CompassPoint.fromBearing(343, CompassRose.SIXTEEN_POINT), is(CompassPoint.NNW));

As I begin implementing the code to solve this problem, all I have is the table of compass points on the Boxing the Compass page. I don’t know what the actual algorithm is that I will be using. Since I have a table of data describing the desired and a function that I need to implement, JUnit’s parameterized tests seem like a good choice to help me use TDD to drive out the algorithm’s implementation.

To start out, I created the shell of the CompassPoint and CompassRose. I’ve chosen to use enums for the implementation. That implementation detail isn’t important for this example, but in retrospect, it somewhat interesting on its own as it ends up roughly being a form of the strategy pattern using enums, albeit a tightly couple one. Also for the example, I’m only showing eight and sixteen-point data to save space as the additional data points for four and 32-point don’t add anything to the example. The code we will create will work with four and 32-point compass roses as well.

Enough of the background. Here is the shell of the code we start with.

public enum CompassPoint {
 
	N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW;
 
	public enum CompassRose {
 
		EIGHT_POINT(8), SIXTEEN_POINT(16);
 
		private int points;
 
		private CompassRose(int points) {
			this.points = points;
		}
	}
 
	public String abbreviation() {
		return name();
	}
 
	public static CompassPoint fromBearing(double bearing,
			CompassRose compassRose) {
		// TODO This is the algorithm we want to implement
		return null;
	}
}

Defining the Parameterized Test

TestNG is the framework that made parameterized tests popular to the mainstream, or if not the mainstream at least to me. JUnit added their own JUnit’s parameterized testing capabilities in version 4, which at this point has been around for a while. Implementing parameterized tests is relatively straightforward but they are definitely more involved than writing a simple test case. There are five components, or steps, that you need to consider.

  • Annotate your test class with @RunWith(Parameterized.class)
  • Create a public static method annotated with @Parameters that returns a Collection of Objects that make up your test data set.
  • Create a public constructor that takes in what is equivalent to one “row” of your test data.
  • Create an instance variable for each “column” of test data.
  • Create your tests case(s) as you normally would using the instance variables as the source of the test data. The test case will be invoked once per each row of data.

I’ll step through each one of these with our concrete example.

@RunWith(Parameterized.class)

Parameterized is a custom JUnit Runner. Simply add the @RunWith(Parameterized.class) annotation to your test class to have JUnit run it as a parameterized test.

@RunWith(Parameterized.class)
public class CompassPointTest {

@Parameters Method

When your test class has the @RunWith(Parameterized.class) annotation, JUnit will looks for a public static method annotated with @Parameter. As I mentioned above, think of this method as returning your test data set. The method must have a return type of Collection<. If you think of your test data as a table of values, each element in the Collection is a row and each index in the Object array is a column.

For our example, I copied the table at Boxing the Compass into a spreadsheet and manipulated it to get it into the format needed by JUnit. The result is.

 
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] { //
                { SIXTEEN_POINT, 0, N }, // center
                        { SIXTEEN_POINT, 11.24, N }, // high
                        { SIXTEEN_POINT, 11.25, NNE }, // low
                        { SIXTEEN_POINT, 22.5, NNE }, // center
                        { SIXTEEN_POINT, 33.74, NNE }, // high
                        { SIXTEEN_POINT, 33.75, NE }, // low
                        { SIXTEEN_POINT, 45, NE }, // center
                        { SIXTEEN_POINT, 56.24, NE }, // high
                        { SIXTEEN_POINT, 56.25, ENE }, // low
                        { SIXTEEN_POINT, 67.5, ENE }, // center
                        { SIXTEEN_POINT, 78.74, ENE }, // high
                        { SIXTEEN_POINT, 78.75, E }, // low
                        { SIXTEEN_POINT, 90, E }, // center
                        { SIXTEEN_POINT, 101.24, E }, // high
                        { SIXTEEN_POINT, 101.25, ESE },// low
                        { SIXTEEN_POINT, 112.5, ESE },// center
                        { SIXTEEN_POINT, 123.74, ESE },// high
                        { SIXTEEN_POINT, 123.75, SE }, // low
                        { SIXTEEN_POINT, 135, SE }, // center
                        { SIXTEEN_POINT, 146.24, SE }, // high
                        { SIXTEEN_POINT, 146.25, SSE }, // low
                        { SIXTEEN_POINT, 157.5, SSE }, // center
                        { SIXTEEN_POINT, 168.74, SSE }, // high
                        { SIXTEEN_POINT, 168.75, S }, // low
                        { SIXTEEN_POINT, 180, S }, // center
                        { SIXTEEN_POINT, 191.24, S }, // high
                        { SIXTEEN_POINT, 191.25, SSW }, // low
                        { SIXTEEN_POINT, 202.5, SSW }, // center
                        { SIXTEEN_POINT, 213.74, SSW }, // high
                        { SIXTEEN_POINT, 213.75, SW }, // low
                        { SIXTEEN_POINT, 225, SW }, // center
                        { SIXTEEN_POINT, 236.24, SW }, // high
                        { SIXTEEN_POINT, 236.25, WSW }, // low
                        { SIXTEEN_POINT, 247.5, WSW }, // center
                        { SIXTEEN_POINT, 258.74, WSW }, // high
                        { SIXTEEN_POINT, 258.75, W }, // low
                        { SIXTEEN_POINT, 270, W }, // center
                        { SIXTEEN_POINT, 281.24, W }, // high
                        { SIXTEEN_POINT, 281.25, WNW }, // low
                        { SIXTEEN_POINT, 292.5, WNW }, // center
                        { SIXTEEN_POINT, 303.74, WNW }, // high
                        { SIXTEEN_POINT, 303.75, NW }, // low
                        { SIXTEEN_POINT, 315, NW }, // center
                        { SIXTEEN_POINT, 326.24, NW }, // high
                        { SIXTEEN_POINT, 326.25, NNW }, // low
                        { SIXTEEN_POINT, 337.5, NNW }, // center
                        { SIXTEEN_POINT, 348.74, NNW }, // high
                        { SIXTEEN_POINT, 348.75, N }, // low
                        { SIXTEEN_POINT, 360, N }, // center
 
                        { EIGHT_POINT, 0, N }, // center
                        { EIGHT_POINT, 22.4, N }, // high
                        { EIGHT_POINT, 22.5, NE }, // low
                        { EIGHT_POINT, 45, NE }, // center
                        { EIGHT_POINT, 67.4, NE }, // high
                        { EIGHT_POINT, 67.5, E }, // low
                        { EIGHT_POINT, 90, E }, // center
                        { EIGHT_POINT, 112.4, E }, // high
                        { EIGHT_POINT, 112.5, SE }, // low
                        { EIGHT_POINT, 135, SE }, // center
                        { EIGHT_POINT, 157.4, SE }, // high
                        { EIGHT_POINT, 157.5, S }, // low
                        { EIGHT_POINT, 180, S }, // center
                        { EIGHT_POINT, 202.4, S }, // high
                        { EIGHT_POINT, 202.5, SW }, // low
                        { EIGHT_POINT, 225, SW }, // center
                        { EIGHT_POINT, 247.4, SW }, // high
                        { EIGHT_POINT, 247.5, W }, // low
                        { EIGHT_POINT, 270, W }, // center
                        { EIGHT_POINT, 292.4, W }, // high
                        { EIGHT_POINT, 292.5, NW }, // low
                        { EIGHT_POINT, 315, NW }, // center
                        { EIGHT_POINT, 337.4, NW }, // high
                        { EIGHT_POINT, 337.5, N }, // "low"
                        { EIGHT_POINT, 60, N }, // center
                });
    }

Each row is data for a single test case. For example, the first row maps to the case "on a 16 point compass rose, 0 maps to North."

The elements in each individual Object array will map the the parameters to the test's constructor. I'll explain that next.

Constructor and Instance Variables

For each element the the Collection returned by our @Parameters, JUnit will instantiate a new test class and call the constructor using the value or values in that element. In our case, each element contains an array of Objects but the framework is flexible so a single String or int works equally as well.

Take an example row in our test data: { SIXTEEN_POINT, 56.24, NE }. The first column is an input CompassRose value. The second column is the input bearing in degrees. And the third column is the expected output Compass point. Our constructor take these values and stores them in corresponding instance variables that the test will access when it executes.

    private CompassRose compassRose;
    private double degrees;
    private CompassPoint expectedCompassPoint;
 
    public CompassPointTest(CompassRose compassRose, double degrees,
            CompassPoint expectedCompassPoint) {
        this.compassRose = compassRose;
        this.degrees = degrees;
        this.expectedCompassPoint = expectedCompassPoint;
    }

The Test Case

We finally arrive at our test case. The test case definition is no different than your standard JUnit test case. This test will be executed once for each element in your data set. The values that drive the test case are the instance variables populated via the constructor that JUnit calls.

    @Test
    public void compassDirectionShouldMatchDegrees() {
        CompassPoint point = CompassPoint.fromBearing(degrees, compassRose);
        assertThat(point, is(expectedCompassPoint));
    }

During the first execution of this test method, compassRose = SIXTEEN_POINT, degrees = 0, and expectedCompassPoint = N given the data supplied by the method with the @Parameters annotation.

On a side note, you can make the error messages provide additional information about what the actual data was that cause the test to fail. When the example above fails, the default JUnit output looks like the following.

java.lang.AssertionError:
Expected: is <N>
     got: <NE>
	at org.junit.Assert.assertThat(Assert.java:778)
        ....

Notice that you don't know what data actually cause the test to fail. You can leverage the description that can be provided to the assertThat (or any other) assertion method to enhance the message.

    @Test
    public void compassDirectionShouldMatchDegrees() {
        CompassPoint point = CompassPoint.fromBearing(degrees, compassRose);
        assertThat(describeExpectation(), point, is(expectedCompassPoint));
    }
 
    private String describeExpectation() {
        return "On a(n) " + compassRose + " compass rose, " + degrees
                + " degrees should translate to " + expectedCompassPoint;
    }

Now the error message has more context on the data that caused the failure.

java.lang.AssertionError: On a(n) EIGHT_POINT compass rose, 60.0 degrees should translate to N
Expected: is <N>
     got: <NE>
	at org.junit.Assert.assertThat(Assert.java:778)
       ...

The Entire Test

Here is the entire test class for reference.

package com.davidehringer.geo;
 
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static com.davidehringer.geo.CompassPoint.CompassRose.*;
import static com.davidehringer.geo.CompassPoint.*;
 
import java.util.Arrays;
import java.util.Collection;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
 
import com.davidehringer.geo.CompassPoint.CompassRose;
 
@RunWith(Parameterized.class)
public class CompassPointTest {
 
    private CompassRose compassRose;
    private double degrees;
    private CompassPoint expectedCompassPoint;
 
    public CompassPointTest(CompassRose compassRose, double degrees,
            CompassPoint expectedCompassPoint) {
        this.compassRose = compassRose;
        this.degrees = degrees;
        this.expectedCompassPoint = expectedCompassPoint;
    }
 
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] { //
                { SIXTEEN_POINT, 0, N }, // center
                        { SIXTEEN_POINT, 11.24, N }, // high
                        { SIXTEEN_POINT, 11.25, NNE }, // low
                        { SIXTEEN_POINT, 22.5, NNE }, // center
                        { SIXTEEN_POINT, 33.74, NNE }, // high
                        { SIXTEEN_POINT, 33.75, NE }, // low
                        { SIXTEEN_POINT, 45, NE }, // center
                        { SIXTEEN_POINT, 56.24, NE }, // high
                        { SIXTEEN_POINT, 56.25, ENE }, // low
                        { SIXTEEN_POINT, 67.5, ENE }, // center
                        { SIXTEEN_POINT, 78.74, ENE }, // high
                        { SIXTEEN_POINT, 78.75, E }, // low
                        { SIXTEEN_POINT, 90, E }, // center
                        { SIXTEEN_POINT, 101.24, E }, // high
                        { SIXTEEN_POINT, 101.25, ESE },// low
                        { SIXTEEN_POINT, 112.5, ESE },// center
                        { SIXTEEN_POINT, 123.74, ESE },// high
                        { SIXTEEN_POINT, 123.75, SE }, // low
                        { SIXTEEN_POINT, 135, SE }, // center
                        { SIXTEEN_POINT, 146.24, SE }, // high
                        { SIXTEEN_POINT, 146.25, SSE }, // low
                        { SIXTEEN_POINT, 157.5, SSE }, // center
                        { SIXTEEN_POINT, 168.74, SSE }, // high
                        { SIXTEEN_POINT, 168.75, S }, // low
                        { SIXTEEN_POINT, 180, S }, // center
                        { SIXTEEN_POINT, 191.24, S }, // high
                        { SIXTEEN_POINT, 191.25, SSW }, // low
                        { SIXTEEN_POINT, 202.5, SSW }, // center
                        { SIXTEEN_POINT, 213.74, SSW }, // high
                        { SIXTEEN_POINT, 213.75, SW }, // low
                        { SIXTEEN_POINT, 225, SW }, // center
                        { SIXTEEN_POINT, 236.24, SW }, // high
                        { SIXTEEN_POINT, 236.25, WSW }, // low
                        { SIXTEEN_POINT, 247.5, WSW }, // center
                        { SIXTEEN_POINT, 258.74, WSW }, // high
                        { SIXTEEN_POINT, 258.75, W }, // low
                        { SIXTEEN_POINT, 270, W }, // center
                        { SIXTEEN_POINT, 281.24, W }, // high
                        { SIXTEEN_POINT, 281.25, WNW }, // low
                        { SIXTEEN_POINT, 292.5, WNW }, // center
                        { SIXTEEN_POINT, 303.74, WNW }, // high
                        { SIXTEEN_POINT, 303.75, NW }, // low
                        { SIXTEEN_POINT, 315, NW }, // center
                        { SIXTEEN_POINT, 326.24, NW }, // high
                        { SIXTEEN_POINT, 326.25, NNW }, // low
                        { SIXTEEN_POINT, 337.5, NNW }, // center
                        { SIXTEEN_POINT, 348.74, NNW }, // high
                        { SIXTEEN_POINT, 348.75, N }, // low
                        { SIXTEEN_POINT, 360, N }, // center
 
                        { EIGHT_POINT, 0, N }, // center
                        { EIGHT_POINT, 22.4, N }, // high
                        { EIGHT_POINT, 22.5, NE }, // low
                        { EIGHT_POINT, 45, NE }, // center
                        { EIGHT_POINT, 67.4, NE }, // high
                        { EIGHT_POINT, 67.5, E }, // low
                        { EIGHT_POINT, 90, E }, // center
                        { EIGHT_POINT, 112.4, E }, // high
                        { EIGHT_POINT, 112.5, SE }, // low
                        { EIGHT_POINT, 135, SE }, // center
                        { EIGHT_POINT, 157.4, SE }, // high
                        { EIGHT_POINT, 157.5, S }, // low
                        { EIGHT_POINT, 180, S }, // center
                        { EIGHT_POINT, 202.4, S }, // high
                        { EIGHT_POINT, 202.5, SW }, // low
                        { EIGHT_POINT, 225, SW }, // center
                        { EIGHT_POINT, 247.4, SW }, // high
                        { EIGHT_POINT, 247.5, W }, // low
                        { EIGHT_POINT, 270, W }, // center
                        { EIGHT_POINT, 292.4, W }, // high
                        { EIGHT_POINT, 292.5, NW }, // low
                        { EIGHT_POINT, 315, NW }, // center
                        { EIGHT_POINT, 337.4, NW }, // high
                        { EIGHT_POINT, 337.5, N }, // "low"
                        { EIGHT_POINT, 60, N }, // center
                });
    }
 
    @Test
    public void compassDirectionShouldMatchDegrees() {
        CompassPoint point = CompassPoint.fromBearing(degrees, compassRose);
        assertThat(describeExpectation(), point, is(expectedCompassPoint));
    }
 
    private String describeExpectation() {
        return "On a(n) " + compassRose + " compass rose, " + degrees
                + " degrees should translate to " + expectedCompassPoint;
    }
}

If you run the test, you will notice that JUnit will actually execute an instance of the test for each row in your test data. Unfortunately, the name of the test will simply be the index of the element in the Collection. As far as I know the test name cannot be customized to provide better context. If you are generating specifications from your test cases, this would be highly desirable since the index number means nothing from a specification perspective. If you use Eclipse, you'll see something similar to the following.
eclipse junit 269x300 Parameterized Tests in JUnit 4.x

The Implementation

At this point, I've covered everything you need to know about parameterized tests in JUnit. But from a TDD process perspective, we still have to implement the code that will make the tests pass. I'm not going to walk through the process for figuring out the algorithm and implementing the code since that is beyond the scope of this article. But I will show you the final code since I know you are dying to know how to convert a bearing to a compass point. Thanks goes out to my brother Geoff for helping me actually get the algorithm right!

package com.davidehringer.geo;
 
import org.apache.commons.lang.Validate;
 
public enum CompassPoint {
 
    N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW;
 
    public enum CompassRose {
 
        EIGHT_POINT(8, new CompassPoint[] { N, NE, E, SE, S, SW, W, NW }), SIXTEEN_POINT(
                16, new CompassPoint[] { N, NNE, NE, ENE, E, ESE, SE, SSE, S,
                        SSW, SW, WSW, W, WNW, NW, NNW });
 
        private int points;
        private CompassPoint[] lookupTable;
 
        private CompassRose(int points, CompassPoint[] lookupTable) {
            this.points = points;
            this.lookupTable = lookupTable;
        }
 
        public CompassPoint getPoint(int index) {
            if (index >= lookupTable.length) {
                throw new IllegalArgumentException("Invalid index: " + index
                        + ". Max index is " + (lookupTable.length - 1));
            }
            return lookupTable[index];
        }
    }
 
    public String abbreviation() {
        return name();
    }
 
    public static CompassPoint fromBearing(double bearing,
            CompassRose compassRose) {
        Validate.isTrue(bearing >= 0);
        Validate.isTrue(bearing <= 360);
 
        double shift = 360.0 / compassRose.points / 2.0;
        double normalizedBearing = (bearing + shift);
 
        int index = (int) (normalizedBearing / (360.0 / compassRose.points))
                % compassRose.points;
        return compassRose.getPoint(index);
    }
}

As shown previously, for the client, it is as simple as the following snippet.

CompassPoint point = CompassPoint.fromBearing(14.65, CompassRose.EIGHT_POINT);
String abbr = point.abbreviation();
...

In Closing

While not new, JUnit's parameterized tests are a very useful tool to have in your testing toolbox.


Leave a Reply