Writing Parameterized Tests With JUnit 4
After we have finished this lesson, we
- Know how we can write parameterized tests by using the test runner provided by JUnit 4.
- Understand why we shouldn’t write parameterized tests that use the test runner provided by JUnit 4.
- Can write parameterized tests by using the JUnitParams library.
This lesson assumes that:
- You are familiar with JUnit 4
- You can use custom test runners
- You know how you can run unit tests by using Maven or Gradle
Watch the Lesson
The text version of this lesson is given in the following:
What Is a Parameterized Test?
A parameterized test is a test case that is invoked by using a predefined input and expected output. These tests are typically used to test classes that have no external dependencies and that provide methods which have no side effects.
Let’s move on and take a look at the system under test.
Introduction to System Under Test
During this lesson we will write unit tests for a simple Calculator
class. It has one method called sum()
and this method returns the sum of two integers
. The source code of the Calculator
class looks as follows:
public class Calculator { public int sum(int first, int second) { return first + second; } }
Next, we will write parameterized tests for the sum()
method by using JUnit 4.
Writing Parameterized Tests With JUnit 4
We can write parameterized tests with JUnit 4 by following these steps:
First, we have to ensure that our unit tests are run by the Parameterized
class. After we have configured the used test runner, the source code of our test class looks as follows:
import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) @Category(UnitTest.class) public class StandardCalculatorTest { }
Second, we have to create a static method that returns a Collection
of Object
arrays, and annotate this method with the @Parameters
annotation. This method creates the input provided to the tested object and describes the expected output.
After we have created this method, the source code of our test class looks as follows:
import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; @RunWith(Parameterized.class) @Category(UnitTest.class) public class StandardCalculatorTest { @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 } }); } }
Third, we have to add the required fields into our test class. These fields contain the tested object, the input provided to the tested object, and the expected output.
After we have added these fields into our test class, its source code looks as follows:
import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; import static org.assertj.core.api.Assertions.assertThat; @RunWith(Parameterized.class) @Category(UnitTest.class) public class StandardCalculatorTest { @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 } }); } private final Calculator calculator; private final int first; private final int second; private final int expectedSum; }
Fourth, we have to write a constructor that takes the input and output data as constructor arguments. These arguments are provided by the custom test runner which obtains them by invoking the data()
method. This constructor also creates the tested object.
After we have created this constructor, the source code of our test class looks as follows:
import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; import static org.assertj.core.api.Assertions.assertThat; @RunWith(Parameterized.class) @Category(UnitTest.class) public class StandardCalculatorTest { @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 } }); } private final Calculator calculator; private final int first; private final int second; private final int expectedSum; public StandardCalculatorTest(int first, int second, int expectedSum) { this.calculator = new Calculator(); this.first = first; this.second = second; this.expectedSum = expectedSum; } }
Fifth, we have to write one unit test which verifies that the sum()
method of the Calculator
class is working as expected when we use the test data provided by the data()
method.
After we have written this unit test, the source code of our test class looks as follows:
import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; import static org.assertj.core.api.Assertions.assertThat; @RunWith(Parameterized.class) @Category(UnitTest.class) public class StandardCalculatorTest { @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 } }); } private final Calculator calculator; private final int first; private final int second; private final int expectedSum; public StandardCalculatorTest(int first, int second, int expectedSum) { this.calculator = new Calculator(); this.first = first; this.second = second; this.expectedSum = expectedSum; } @Test public void shouldReturnCorrectSum() { int actualSum = calculator.sum(first, second); assertThat(actualSum).isEqualByComparingTo(expectedSum); } }
When we run this test method, we notice that the shouldReturnCorrectSum()
method is invoked twice by using the parameters which are configured in the data()
method.
Additional Reading:
Even though this approach works, I think that we shouldn’t use it because of the following reasons:
- It requires a lot of boilerplate code. This makes our test hard to write and read.
- Because the test data is passed as constructor arguments, we cannot write multiple test methods which use different test data.
Luckily for us, there is a better way. Let’s find out how we can simplify our parameterized tests by using the JUnitParams library.
Writing Parameterized Tests With JUnitParams
Getting the Required Dependencies
Before we can write parameterized tests with JUnitParams, we have to declare the JUnitParams
dependency in our build script.
If we are using Maven, we have to add the following snippet into our pom.xml file:
<dependency> <groupId>pl.pragmatists</groupId> <artifactId>JUnitParams</artifactId> <version>1.0.5</version> <scope>test</scope> </dependency>
If we are using Gradle, we have to add the following snippet into our build.gradle file:
testCompile( 'pl.pragmatists:JUnitParams:1.0.5' )
Let’s move on and write the same unit test by using the JUnitParams library.
Writing the Unit Test
We can ensure that the sum()
method of the Calculator
class is working as expected by following these steps:
First, we have to ensure that our unit tests are run by the JUnitParamsRunner
class. After we have configured the used test runner, the source code of our test class looks as follows:
import junitparams.JUnitParamsRunner; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(JUnitParamsRunner.class) @Category(UnitTest.class) public class JUnitParamsCalculatorTest { }
Second, we have to write one test method by following these steps:
- Add a new test method into our test class. This method takes the provided input and the expected output as method parameters.
- Ensure that
sum()
method of theCalculator
class is working as expected.
After we have written this test method, the source code of our test class looks as follows:
import junitparams.JUnitParamsRunner; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import static org.assertj.core.api.Assertions.assertThat; @RunWith(JUnitParamsRunner.class) @Category(UnitTest.class) public class JUnitParamsCalculatorTest { @Test public void shouldReturnCorrectSum(int first, int second, int expectedSum) { Calculator calculator = new Calculator(); int actualSum = calculator.sum(first, second); assertThat(actualSum).isEqualByComparingTo(expectedSum); } }
Third, we have to configure the parameters that are passed to our test method by following these steps:
- Annotate the test method with the
@Parameters
annotation. - Configure the input provided to the
Calculator
object and the expected output.
After we have configured the parameters of our parameterized test, the source code of our test class looks as follows:
import junitparams.JUnitParamsRunner; import junitparams.Parameters; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import static org.assertj.core.api.Assertions.assertThat; @RunWith(JUnitParamsRunner.class) @Category(UnitTest.class) public class JUnitParamsCalculatorTest { @Test @Parameters({ "0, 0, 0", "1, 1, 2" }) public void shouldReturnCorrectSum(int first, int second, int expectedSum) { Calculator calculator = new Calculator(); int actualSum = calculator.sum(first, second); assertThat(actualSum).isEqualByComparingTo(expectedSum); } }
As we can see, JUnitParams solves the problems which we found from the parameterized tests that use the test runner provided by JUnit. It has no boilerplate code, and because the parameters are passed directly to the test method, we can have multiple test methods that use different parameters. We can even add normal test methods into our test class.
Also, one additional benefit of using JUnitParams is that we can implement custom data providers and read input data from external data sources such as CSV files.
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- We shouldn’t use the test runner provided by JUnit 4 because it forces us to write boilerplate code that makes our tests hard to write and read.
- If we want to use the JUnitParams library, we have to declare the
JUnitParams
dependency in our build script. - The JUnitParams library solves the problems caused by JUnit 4.
- The JUnitParams library supports custom data provider classes and external data sources such as CSV files.
Previous LessonNext Lesson