JUnit Parameterized and named tests
A relatively simple, working and complete example of JUnit Parameterized named tests.
Advantages of Parameterized in JUnit testing:
- Re-use the same test methods over and over, just changing the parameters.
- The method annotated by
@Parameters
could return a collection of data from a spreadsheet, database or hard-coded variable. - New naming mechanism lets you add a sensible label to each test to make it easy to identify the one that failed.
import static org.junit.Assert.assertEquals; 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; @RunWith(Parameterized.class) public final class MultiplicationParameterizedTest { private final int expectedResult; private final int firstNumber; private final int secondNumber; public MultiplicationParameterizedTest(final int theExpectedResult, final int theFirstNumber, final int theSecondNumber) { expectedResult = theExpectedResult; firstNumber = theFirstNumber; secondNumber = theSecondNumber; } @Parameters(name = "Multiplication test {index}: {0}={1}x{2}") public static Collection<Integer[]> numbersToBeMultiplied() { return Arrays.asList(new Integer[][] { { 10, 5, 2 }, { 48, 6, 8 }, { 1452, 33, 44 }, { 1044, 87, 12 }, { 135, 3, 45 }, }); } @Test public void sum() { final int actualResult = multiplyNumbers(firstNumber, secondNumber); assertEquals("Expected [" + firstNumber + " * " + secondNumber + " = " + expectedResult + "], not [" + actualResult + "]", expectedResult, actualResult); } public int multiplyNumbers(int a, int b) { int product = a * b; return product; } }
Here is what the tests look like when they pass in Eclipse. It shows you useful the naming mechanism is.
Now I change the last test case just to show what a failure looks like.
@Parameters(name = "Multiplication test {index}: {0}={1}x{2}") public static CollectionnumbersToBeMultiplied() { return Arrays.asList(new Integer[][] { { 10, 5, 2 }, { 48, 6, 8 }, { 1452, 33, 44 }, { 1044, 87, 12 }, { 13, 3, 45 }, }); }
And in Eclipse, that failure looks like this:
You can still use setup and tear down methods:
@Before public void setup() { System.out.println("Set up [" + firstNumber + " * " + secondNumber + " = " + expectedResult + "]."); } @After public void tearDown() { System.out.println("Tear down [" + firstNumber + " * " + secondNumber + " = " + expectedResult + "]."); }
Note that names feature was introduced in JUnit 4.11, which is the latest stable build at the time of writing (Tuesday 21 October 2014).
What about data from a spreadsheet with lots of columns?
I am thinking of using this for tests that will have a lot of data that I want to write in a spreadsheet of ten columns or more. One problem I have with the above example is that I have a constructor accepting one parameter for each column of data. I need to encapsulate that in a class. This is because I am thinking of using this to drive Selenium tests, where I need lots of data to enter into every field in the UI. The example below shows part of how I will handle this.
import static org.junit.Assert.assertEquals; 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; @RunWith(Parameterized.class) public final class MultiplicationParameterizedTestWithObject { private final DataUnderTest data; public MultiplicationParameterizedTestWithObject(final DataUnderTest theData) { data = theData; } @Parameters(name = "Multiplication test {index}: {0}") public static Collection<DataUnderTest[]> numbersToBeMultiplied() { return Arrays.asList(new DataUnderTest[][] { { new DataUnderTest("10 = 5 * 2", 10, 5, 2) }, { new DataUnderTest("48 = 6 * 8", 48, 6, 8) }, { new DataUnderTest("1,452 = 33 * 44", 1452, 33, 44) }, { new DataUnderTest("1,044 = 87 * 12", 1044, 87, 12) }, { new DataUnderTest("135 = 3 * 45", 135, 3, 45) }, }); } @Test public void sum() { final int firstNumber = data.getFirstNumber(); final int secondNumber = data.getSecondNumber(); final int actualResult = multiplyNumbers(firstNumber, secondNumber); final int expectedResult = data.getExpectedResult(); assertEquals("Expected [" + firstNumber + " * " + secondNumber + " = " + expectedResult + "], not [" + actualResult + "]", expectedResult, actualResult); } public int multiplyNumbers(int a, int b) { int product = a * b; return product; } private static class DataUnderTest { private final String label; private final int expectedResult; private final int firstNumber; private final int secondNumber; public DataUnderTest(final String theLabel, final int theExpectedResult, final int theFirstNumber, final int theSecondNumber) { label = theLabel; expectedResult = theExpectedResult; firstNumber = theFirstNumber; secondNumber = theSecondNumber; } @Override public String toString() { return label; } public String getLabel() { return label; } public int getExpectedResult() { return expectedResult; } public int getFirstNumber() { return firstNumber; } public int getSecondNumber() { return secondNumber; } } }
What I will need to do differently from above in my real test cases:
DataUnderTest
will be much bigger - big enough that it will need to be in a separate java file.- The method annotated with
@Parameters
will read data from an Excel spreadsheet, perhaps using Java Excel API or The Apache POI Project.
What I like about this approach:
- JUnit Parameterized seems to scale nicely because the method annotated with
@Parameters
can get data from anywhere. - By using
DataUnderTest.toString()
, I can still make good use of the same naming mechanism:@Parameters(name = "Multiplication test {index}: {0}")
. By using this name declaration, the Eclipse results will look exactly the same as the first example because the {0} will be replaced byDataUnderTest.toString()
. In my actual test, I plan to use the first column of data as the label.
What I don't like about this approach:
- The fact that the method annotated with
@Parameters
has to return an Iterable of arrays, for exampleCollection<DataUnderTest[]>
. It forces me to bind each element of the array to a constructor parameter, which is at least well defined, but doesn't seem so flexible, e.g. I cannot create an array that includesString
andint
without making it an Object array, in which case I would lose type information and would be forced to cast or somehow convert the values back to the types I want.