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.
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.
December 12th, 2019 at 4:35 pm
You said it adequately.. What Is The Cost Of Celexa Generic
December 12th, 2019 at 4:40 pm
Thank you! Great stuff! retin-a cream
December 12th, 2019 at 4:44 pm
Regards. I enjoy it! ventoline furosemide bnf Female Viagra 50mg Cymbalta And Weaning Off
December 12th, 2019 at 4:45 pm
Lovely material. Regards! celexa for anxiety
December 12th, 2019 at 4:47 pm
Good forum posts. Many thanks! aciclovir
December 12th, 2019 at 4:54 pm
Lovely info, Appreciate it. [url=https://safeonlinecanadian.com/]canada prescription drugs[/url]
December 12th, 2019 at 5:01 pm
Thanks a lot! Great stuff! [url=https://canadianonlinepharmacytrust.com/]trust pharmacy canada[/url]
December 12th, 2019 at 5:02 pm
You actually revealed this well! Amoxicillin 600 Mg Dosage For Children
December 12th, 2019 at 5:03 pm
Superb facts. Thanks a lot! lisinopril generic
December 12th, 2019 at 5:07 pm
Wow a good deal of beneficial facts. metformin 500 mg
December 12th, 2019 at 5:13 pm
Thanks, I value it. https://amoxicillincaamoxil.com/
December 12th, 2019 at 5:14 pm
Cheers, A lot of tips!
Lisinopril 30 Mg No Prescription
December 12th, 2019 at 5:23 pm
Wonderful data. Thanks a lot! Prednisone Stomach Fat
December 12th, 2019 at 5:29 pm
Nicely put. Thank you! bactrim ds
December 12th, 2019 at 5:29 pm
Really lots of useful knowledge! [url=https://viagrabestbuyrx.com/]canadian pharmacy[/url]
December 12th, 2019 at 5:37 pm
Incredible a good deal of good information! furosemida
December 12th, 2019 at 5:53 pm
Thank you, Very good stuff. drugs for sale
December 12th, 2019 at 5:53 pm
You mentioned it exceptionally well. https://prednisone-20mg-pills.com/
December 12th, 2019 at 6:00 pm
You said it perfectly.. doxycycline bnf
December 12th, 2019 at 6:02 pm
You said it adequately.! https://valsartanhydrochlorothiazide.com/
December 12th, 2019 at 6:05 pm
Wonderful content. Thank you. order viagra online without prescription
December 12th, 2019 at 6:06 pm
Nicely put, Regards! advair coupon bactrim ds 800-160 gabapentina hydroxyzine hcl finasteride doxycycline for dogs
December 12th, 2019 at 6:08 pm
Effectively voiced of course! . Buy Neurontin Paypal
December 12th, 2019 at 6:15 pm
Kudos. A good amount of information!
Diflucan And Spleen Should I Get Back On Celexa bactrim ds 800-160
December 12th, 2019 at 6:19 pm
Awesome facts. Many thanks. price pro pharmacy canada
December 12th, 2019 at 6:22 pm
Thank you! Great stuff. albuterol sulfate inhaler augmentin Actos Plus Metformin Coupons Getting Off Lexapro Weight lisinopril generic
December 12th, 2019 at 6:31 pm
Terrific tips. Many thanks! kamagra bestellen deutschland
December 12th, 2019 at 6:36 pm
more info…
Get now the best synchronized clock system that is this week and available plus in stock now in addition reasonably priced now only!…
December 12th, 2019 at 6:42 pm
Kudos. I enjoy it! https://valsartanhydrochlorothiazide.com/
December 12th, 2019 at 6:49 pm
You actually mentioned this very well. https://viaonlinebuyntx.com/
December 12th, 2019 at 6:51 pm
Fine info. Thank you! tretinoin
December 12th, 2019 at 6:55 pm
You actually suggested this well! Is Baclofen Used For Trigeminal Neuralgia
December 12th, 2019 at 6:56 pm
Helpful material. Thank you. Valacyclovir Buy Uk
December 12th, 2019 at 6:57 pm
Kudos, Helpful information. canadian online pharmacy promethazine hydrochloride
December 12th, 2019 at 7:07 pm
You actually stated that very well! [url=https://ciaonlinebuymsn.com/]canadian pharcharmy online fda approved[/url]
December 12th, 2019 at 7:12 pm
Amazing information. Cheers. aarp recommended canadian pharmacies
December 12th, 2019 at 7:14 pm
Kudos! Useful information. https://azithromycinmaxim.com/
December 12th, 2019 at 7:20 pm
sync clocks for hospitals…
Start using the top school pa system that is this week and available plus in stock in addition with professional installation now only!…