Data driven testing with JUnit parameterized tests
In Mock JUnit tests with Mockito example post, I have introduced Mockito and showed how to use for proper unit testing. In current post I will show how to improve test coverage by adding more scenarios. One solution is to copy and then paste single unit test and change input and expected output values, but this is a failure-prone approach. A smarter approach is needed - data-driven testing.
Data Driven Testing
The term from Wikipedia is: Data-driven testing (DDT) is a term used in the testing of computer software to describe testing done using a table of conditions directly as test inputs and verifiable outputs as well as the process where test environment settings and control are not hard-coded.This exactly what is needed to improve test coverage - test with different scenarios and different input data without hard-coding the scenario itself, but just feeding different input and expected output data to it.
Parameterized JUnit tests
JUnit supports running test or several tests with given data table. Several things have to be done in order to do this:- Annotate the test class
- Define test data
- Define variables to store the test data and read it
- Use tests data in tests
Annotate the class
The class needs to be run with a specialized runner in order to be treated as data-driven one. Runner is: org.junit.runners.Parameterized. The class looks like:import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class LocatorParameterizedTest {
}
Define test data
Test data is seeded from static method: public static Iterable<Object[]> data(). This method returns a collection of Object arrays where each array is one row with input and expected output test data. This method is annotated with @Parameterized.Parameters. The annotation may accept name argument which can display data from each row by its index: name = "{index}: Test with X={0}, Y={1}, result is: {2}", where {index} is current test sequence, {0} is the first element from Object array. Here is how test data is defined:@Parameterized.Parameters(name = "{index}: Test with X={0}, Y={1}, result: {2}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{-1, -1, new Point(1, 1)},
{-1, 0, new Point(1, 0)},
{-1, 1, new Point(1, 1)},
});
}
Define variables to store the test data and read it
Private fields are needed to store every index from Object array representing test data row. In the constructor of the class, those variables are stored. Not that constructor must have the same number of parameters. If there is difference running the test fails with: java.lang.IllegalArgumentException: wrong number of arguments exception. Code is:private final int x;
private final int y;
private final Point expected;
public LocatorParameterizedTest(int x, int y, Point expected, int a) {
this.x = x;
this.y = y;
this.expected = expected;
}
Use tests data in tests
Once read test data is accessed in tests by using the private fields that were read through the constructor:@Test
public void testLocateLocalResult() {
assertTrue(arePointsEqual(expected, locatorUnderTest.locate(x, y)));
}
private boolean arePointsEqual(Point p1, Point p2) {
return p1.getX() == p2.getX()
&& p1.getY() == p2.getY();
}
Putting it all together
Combining all steps into one class leads to unit test shown below. If you switch the tabs you can see original test class with just two tests as described Mock JUnit tests with Mockito example post:import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(Parameterized.class)
public class LocatorParameterizedTest {
private static final Point MOCKED_POINT = new Point(11, 11);
private LocatorService locatorServiceMock = mock(LocatorService.class);
private Locator locatorUnderTest;
@Parameterized.Parameters(name
= "{index}: Test with X={0}, Y={1}, result: {2}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{-1, -1, new Point(1, 1)},
{-1, 0, new Point(1, 0)},
{-1, 1, new Point(1, 1)},
{0, -1, new Point(0, 1)},
{0, 0, MOCKED_POINT},
{0, 1, MOCKED_POINT},
{1, -1, new Point(1, 1)},
{1, 0, MOCKED_POINT},
{1, 1, MOCKED_POINT}
});
}
private final int x;
private final int y;
private final Point expected;
public LocatorParameterizedTest(int x, int y, Point expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
@Before
public void setUp() {
when(locatorServiceMock.geoLocate(any(Point.class)))
.thenReturn(MOCKED_POINT);
locatorUnderTest = new Locator(locatorServiceMock);
}
@Test
public void testLocateResults() {
assertTrue(arePointsEqual(expected,
locatorUnderTest.locate(x, y)));
}
private boolean arePointsEqual(Point p1, Point p2) {
return p1.getX() == p2.getX()
&& p1.getY() == p2.getY();
}
}
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LocatorTest {
private static final Point TEST_POINT = new Point(11, 11);
@Mock
private LocatorService locatorServiceMock;
private Locator locatorUnderTest;
@Before
public void setUp() {
when(locatorServiceMock.geoLocate(any(Point.class)))
.thenReturn(TEST_POINT);
locatorUnderTest = new Locator(locatorServiceMock);
}
@Test
public void testLocateWithServiceResult() {
assertEquals(TEST_POINT, locatorUnderTest.locate(1, 1));
}
@Test
public void testLocateLocalResult() {
Point expected = new Point(1, 1);
assertTrue(arePointsEqual(expected,
locatorUnderTest.locate(-1, -1)));
}
private boolean arePointsEqual(Point p1, Point p2) {
return p1.getX() == p2.getX()
&& p1.getY() == p2.getY();
}
}
The full example can be found in LocatorParameterizedTest.java class.