Tuesday, October 21, 2014

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 Collection numbersToBeMultiplied() {
   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 by DataUnderTest.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 example Collection<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 includes String and int 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.