In my previous post on this topic, covered writing the parameterized testcases with @Parameters
annotation. If I pick the correct words then that approach was quite messy and not so readable. It required a lot of attention un-necessarily. Well, there is another approach using which you can write parameterized testcases in Junit with help of annotations like @Theory
And @DataPoints
.
I will take example from previous post, and convert it to new approach. It just make sense because after this we will be able to compare, what got changed and how much it is different from previous approach.
1) Feeding input data with @DataPoints
Here, only annotation has been changed @Parameters
to @DataPoints
. Rest the concept is same.
Previously, method to feed input was:
@Parameters(name = "Run #Square of : {0}^2={1}") public static Iterable<Object []> data() { return Arrays.asList(new Object[][] { { 1, 1 }, { 2, 4 }, { 3, 19 }, { 4, 16 }, { 5, 25 } }); }
Now it is:
@DataPoints public static int[][] integers() { return new int[][]{{1, 1}, {2, 4}, {3, 9}, {4, 16}, {5, 25}, {}}; }
Please note that you can write the inputs separately using @DataPoint
annotation.
@DataPoint public static int[] input6 = new int[]{6, 36}; @DataPoint public static int[] input7 = new int[]{7, 49};
I have changed return type from “Iterable<object[]>” to “int[][]” because of slight difference in how these inputs are feed to testcases. You will see the difference in next section.
2) Writing Testcases with @Theory
Structurally, a theory-based class is simpler than a parameterized test class. The class declaration should be annotated with @RunWith(Theories.class), and it must provide two entities:
- A data method that generates and returns test data
- A theory
The data method must be annotated with @DataPoints, and each theory must be annotated with @Theory. As with an ordinary unit test, each theory should contain at least one assertion.
In previous approach, we wrote the testcase like below:
@Test public void testUserMapping() { // You can use here assert also Assert.assertEquals(resultExpected, MathUtils.square(input)); }
Where input and resultExpected were declared as class members and populated using a parameterized constructor. As you can see that testUserMapping() method above does not take any parameter.
In new approach, tests are annotated with @Theory
annotation. e.g.
@Theory public void testSquares(final int[] inputs) { Assume.assumeTrue(inputs[0] > 0 && inputs[1] > 0); Assert.assertEquals(inputs[1], MathUtils.square(inputs[0])); }
You see that parameters are now part of testcase and this is best part of concept. assumeTrue() ensures that parameters are positive numbers and assertEquals() check the function logic which we need to test.
To tun the above testcase, annotate the class in following manner with @RunWith
.
@RunWith(Theories.class) public class JunitTestsWithParameters { //Testcases }
If you think that some testcases may throw a exception while doing operation, handle them with @Rule
annotation and ExpectedException
class. A more complete working example is given below:
package test.junit.theory; import org.junit.Assert; import org.junit.Assume; import org.junit.Rule; import org.junit.experimental.theories.DataPoint; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; @RunWith(Theories.class) public class JunitTestsWithParameters { @Rule public ExpectedException expectedException = ExpectedException.none(); @DataPoints public static int[][] integers() { return new int[][]{{1, 1}, {2, 4}, {3, 9}, {4, 16}, {5, 25}, {}}; } @DataPoint public static int[] input6 = new int[]{6, 36}; @DataPoint public static int[] input7 = new int[]{7, 49}; @Theory public void testSquares(final int[] inputs) { Assume.assumeTrue(inputs.length == 2); Assume.assumeTrue(inputs[0] > 0 && inputs[1] > 0); Assert.assertEquals(inputs[1], MathUtils.square(inputs[0])); } @Theory public void testBlankArrays(final int[] inputs) { Assume.assumeTrue(inputs.length == 0); expectedException.expect(ArrayIndexOutOfBoundsException.class); Assert.assertEquals(inputs[1], MathUtils.square(inputs[0])); } }
Run the above testcases and result will look like this:

Please note that separation of test data from test/theory implementation can have another positive effect apart from brevity: You might start to think about you test data independent of the actual stuff to test.
But at the same time, You should have noticed, however, that there is no means of pairing a specific result with a specific data point. You should use theories when you can express in the form of an assertion the general relationship between a data point and an expected result, and when that relationship will hold true for all data.
So choose between Theory and parameterized testcase carefully with proper consideration. They are not exact alternative to parameterized testcases, rather they complement them.
Happy Learning !!
Leave a Reply