Be a 10X Developer! Write Parameterized Tests

Following on from Yannick’s previous post about the necessity of thorough testing we wanted to look at the subject from an alternative angle. Within the confines of Junit’s Parameterized Test it is possible to test multiple classes simultaneously – thus saving  time and effort – but as with every labour-saving device, the devil is in the detail at the front end of the task – how to create a test we can rely on that will also reduce our ongoing test maintenance burden? Kichwa Coders’ intern Pierre Sachot grapples with this problem in his latest blog detailing how he set about creating a Parameterized Test within the Eclipse January Project. Find out how he got on and let us know what you think in the comments below.

Context:

I was back on the January Eclipse project, more specifically on JUnit Tests. We needed to test two functions of the Maths.java: arctan2() function and abs() in order to calculate the absolute value of a Dataset. I worked more on the second function, and as those two functions were really similar we decided to create a Parameterized Test class to include the first function too. This is a test that could be applied to both series of code, with only variable changing. It can therefore be used to test a function with a lot of values to find the one which is failing, or in our case, on several class types.

Tests before using Parameterized Tests:

Our tests were really similar:

@Test
public void testAbsDoubleInput() {
  double[] obj = new double[] { 4.2, -2.9, 6.1 };
  Dataset a = DatasetFactory.createFromObject(obj);

  int size = a.getSize();
  double[] c = new double[size];
  for (int i = 0; i < size; i++) {
    double abs = Math.abs(obj[i]);
    c[i] = abs;
  }
  Dataset expectedResult = DatasetFactory.createFromObject(c);

  Dataset actualResult = Maths.abs(a);
  TestUtils.assertDatasetEquals(expectedResult, actualResult, true, ABSERRD, ABSERRD);
}

@Test
public void testAbsbyteInput() {
  Dataset a = DatasetFactory.createFromObject(new byte[] { -4, 8, 6 });

  int size = a.getSize();
  byte[] c = new byte[size];
  for (int i = 0; i < size; i++) {
    int abs = Math.abs(a.getByte(i));
    c[i] = (byte) abs;
  }
  Dataset expectedResult = DatasetFactory.createFromObject(c);

  Dataset actualResult = Maths.abs(a);
  TestUtils.assertDatasetEquals(expectedResult, actualResult, true, ABSERRD, ABSERRD);
}

Here I have shown examples of two of the classes, but we did in fact use it for six. So now we needed to identify what was similar and what was different in all the tests, and what could be changed to make them more similar.

Here the actualResult needed to be in the result type we wanted, because that is what we needed to test, but the expected result type could be written which ever way we wanted it:

public void testAbsDoubleInput() {
  double[] obj = new double[] { 4.2, -2.9, 6.1 };
  Dataset a = DatasetFactory.createFromObject(DoubleDataset.class, obj);

  int size = a.getSize();
  double[] c = new double[size];
  for (int i = 0; i < size; i++) {
    double abs = Math.abs(obj[i]);
    c[i] = abs;
  }
  Dataset expectedResult = DatasetFactory.createFromObject(DoubleDataset.class, c);

  Dataset actualResult = Maths.abs(a);
  TestUtils.assertDatasetEquals(expectedResult, actualResult, true, ABSERRD, ABSERRD);
}

@Test
public void testAbsbyteInput() {
  double[] obj = new double[] { 4.2, -2.9, 6.1 };
  Dataset a = DatasetFactory.createFromObject(ByteDataset.class, obj);

  int size = a.getSize();
  double[] c = new double[size];
  for (int i = 0; i < size; i++) {
    double abs = Math.abs(obj[i]);
    c[i] = abs;
  }
  Dataset expectedResult = DatasetFactory.createFromObject(ByteDataset.class, c);

  Dataset actualResult = Maths.abs(a);
  TestUtils.assertDatasetEquals(expectedResult, actualResult, true, ABSERRD, ABSERRD);
}

Here in the Dataset constructor you can see that we created a ByteDataset from a double array. This was possible because Dataset class allows the user to do this. Now it was possible to see that the only thing that would need to be changed in our tests was the class variable in order to create the Dataset.

We wrote a variable to take the class type like this:

@Test
public void testAbsbyteInput() {
  Class<? extends Dataset> class1 = ByteDataset.class;
  double[] obj = new double[] {4.2, -2.9, 6.10};
  Dataset input = DatasetFactory.createFromObject(class1, obj);
  ...
}

Tests using Parameterized Tests:

So once you can write a parameterize class test you can reduce your code size and simplify your tests:

package org.eclipse.january.dataset;

import org.junit.Test;
import org.eclipse.january.asserts.TestUtils;

import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runner.RunWith;

import java.util.Arrays;
import java.util.Collection;

@RunWith(Parameterized.class)
public class MathsBasicTypeAbsFunctionParameterizeTest {

  @Parameters(name = "{index}: {0}") //the "(name = "{index}: {0}")" allows the Junit test to write which test failed with which parameter
  public static Collection<Object> data() { //called to get the array of variables that need to be change
    return Arrays.asList(
      new Object[] {
        FloatDataset.class,
        DoubleDataset.class,
        ByteDataset.class,
        ShortDataset.class,
        IntegerDataset.class,
        LongDataset.class
      });
  }

  @Parameter
  public Class<? extends Dataset> classType; //Parameter which change when the last test is done.

  //parameter specific to our test don't worry about this one
  private final static double ABSERRD = 1e-8;

  @Test
  public void test() {
    Class<? extends Dataset> class1 = classType;
    double[] obj = new double[] {4.2, -2.9, 6.10};
    Dataset input = DatasetFactory.createFromObject(class1, obj);
    Dataset output = DatasetFactory.createFromObject(class1, new double[]{0,0,0});

    int size = input.getSize();
    double[] c = new double[size];
    for (int i = 0; i < size; i++) {
      double abs = Math.abs(obj[i]);
      c[i] = abs;
    }
    Dataset expectedResult = DatasetFactory.createFromObject(class1, c);

    Dataset actualResult = Maths.abs(input, output);
    TestUtils.assertDatasetEquals(expectedResult, actualResult, true, ABSERRD, ABSERRD);
  }
}

Here the function data() is the one which will be called to change the data type. Now we had  a parameterized test which would work with every class which extended Dataset.

Conclusion:

Once you know how to reduce your tests code and can identify things which are the same between tests, it becomes possible to code tests efficiently, winning back time and avoiding code duplication.  This is why Parameterized Tests are essential for every true and wannabe 10X Developer.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s