Writing A Parameterized Test In JUnit With Examples
JUnit, along with many other unit test frameworks, offers the concept of parameterized tests. Using parameterized tests creates a separation between test data and structure, simplifying test code, eliminating duplication, and ensuring more coverage.
In this post, you’ll learn how to write a parameterized test in JUnit, with plenty of examples. To extract the most value from the post, you should be a Java developer with some experience writing JUnit tests and know how to manage dependencies using Gradle or Maven. It should go without saying, but an installation of Java is essential.
Let’s dig in.
What is a JUnit parameterized test?
In JUnit, a parameterized test is a test method in which the data used in the test comes from parameters instead of being hardcoded in the method itself. There’s a special syntax—in the form of annotations—that allows you to pass a set of values to the test method. When JUnit runs the test, it will execute one test for each set of data passed to the method.
The separation of concerns that parameterized tests provide brings benefits such as:
- Simpler and more readable test code because instead of being hardcoded, many values inside the test are now well-named parameters
- Less test duplication because you can have a single method give the origin to many tests
- More test coverage because the friction to add a new set of data is way smaller than adding a whole new test method
With that out of the way, let’s get started.
Starting with the production code
For you to write tests, you need production code in place. So, start a new Java project using the tools you’re most comfortable with. Make sure to install JUnit. Then, create a class called FizzBuzzer
with the following code:
public class FizzBuzzer {
public String convert(int number) {
if (number <= 0 || number > 1000)
return "N/A";
if (number % 15 == 0)
return "FizzBuzz";
if (number % 3 == 0)
return "Fizz";
if (number % 5 == 0)
return "Buzz";
return Integer.toString(number);
}
}
Code language: Java (java)
Writing a few traditional tests
Let’s start by writing a few regular, non-parameterized tests. Under src/test/java
, create a class called FizzBuzzerTest
. Paste the following code into it:
import org.example.FizzBuzzer;
import org.junit.jupiter.api.Test;
import static org.junit.Assert.*;
public class FizzBuzzerTest {
@Test
public void convert_zeroResultInNonApplicableResponse() {
var sut = new FizzBuzzer();
assertEquals("N/A", sut.convert(0));
}
@Test
public void convert_negativeNumbersResultInNonApplicableResponse() {
var sut = new FizzBuzzer();
assertEquals("N/A", sut.convert(-1));
}
@Test
public void convert_10001ResultsInNonApplicableResponse() {
var sut = new FizzBuzzer();
assertEquals("N/A", sut.convert(1001));
}
}
Code language: Java (java)
The tests above test the method’s sad path. They verify that passing zero, negative numbers, or numbers greater than 1000 result in “N/A” being returned as we expect.
Let’s now add more tests to verify the happy path of the method. We’ll start by showing that the method returns the number itself if it is not a multiple of three or five.
@Test
public void convert_1ResultsInOne() {
var sut = new FizzBuzzer();
assertEquals("1", sut.convert(1));
}
@Test
public void convert_2ResultsInTwo() {
var sut = new FizzBuzzer();
assertEquals("2", sut.convert(2));
}
Code language: Java (java)
After that, let’s verify the three results in “Fizz,” five in “Buzz,” and fifteen in “FizzBuzz”:
@Test
public void convert_3ResultsInFizz() {
var sut = new FizzBuzzer();
assertEquals("Fizz", sut.convert(3));
}
@Test
public void convert_5ResultsInBuzz() {
var sut = new FizzBuzzer();
assertEquals("Buzz", sut.convert(5));
}
@Test
public void convert_15ResultsInFizzBuzz() {
var sut = new FizzBuzzer();
assertEquals("FizzBuzz", sut.convert(15));
}
Code language: Java (java)
All tests pass, but are you really satisfied with them? I know I’m not. And that’s because the tests above doesn’t offer comprehensive coverage. Someone playing devil’s advocate could easily write a perverse implementation that passes that tests while not delivering the right answer in all scenarios.
Let’s add more tests covering multiples of three:
@Test
public void convert_6ResultsInFizz() {
var sut = new FizzBuzzer();
assertEquals("Fizz", sut.convert(6));
}
@Test
public void convert_33ResultsInFizz() {
var sut = new FizzBuzzer();
assertEquals("Fizz", sut.convert(33));
}
Code language: Java (java)
Then, the same for multiples of five:
@Test
public void convert_10ResultsInBuzz() {
var sut = new FizzBuzzer();
assertEquals("Buzz", sut.convert(10));
}
@Test
public void convert_35ResultsInBuzz() {
var sut = new FizzBuzzer();
assertEquals("Buzz", sut.convert(35));
}
Code language: Java (java)
And finally, the same for numbers that are multiples of both three and five—or, in other words, multiples of fifteen:
@Test
public void convert_30ResultsInFizzBuzz() {
var sut = new FizzBuzzer();
assertEquals("FizzBuzz", sut.convert(30));
}
@Test
public void convert_45ResultsInFizzBuzz() {
var sut = new FizzBuzzer();
assertEquals("FizzBuzz", sut.convert(45));
}
Code language: JavaScript (javascript)
All tests pass, which means we’re ready for the next step.
How do you write a JUnit parameterized test?
As you’ve noticed, there’s a lot of repetition in the tests above. For instance, the first three tests, in which the result is “N/A,” are essentially the same. Now it’s at this point where many developers fall into the trap of using a loop to iterate over the input values (in an array, for instance) and doing the assertion in the loop’s body.
Don’t do that.
Unit tests should be as simple as possible. As a guideline, keep their cyclomatic complexity at one. Resist the urge to introduce loops or conventional branching inside tests, because that will increase the complexity of your test method and the likelihood it contains a bug.
The better course of action is to parameterize the test. Let’s do that for the first three methods by adding the following new test:
@Test
public void convert_invalidInputResultsInNonApplicableResponse(int input) {
var sut = new FizzBuzzer();
assertEquals("N/A", sut.convert(input));
}
Code language: JavaScript (javascript)
The new test method receives an int
as an argument, and passes it in turn to the sut.convert()
method. But something is missing: the value has to come from somewhere. Let’s solve that in four steps:
- Add the
junit-jupiter-params
dependency to your project - Add import
org.junit.jupiter.params.provider.ValueSource;
to the top of your class - Replace the
@Test
annotation of the new method with the@ParameterizedTest
one - Add this new annotation to the method:
@ValueSource(ints = {0, -1, 1001})
The @ValueSource
annotation is what provides the values for the input parameter. For each value defined there, JUnit will execute one test. Now you can eliminate the three original tests that verified the “N/A” scenario.
Parameterizing the “happy path” tests
It’s now time to parameterize the happy path tests. First, let’s parameterize the multiples-of-three scenario:
@ParameterizedTest
@ValueSource(ints = {3, 6, 9, 12, 33, 36, 39, 42})
public void convert_multiplesOfThreeResultsInFizz(int input) {
var sut = new FizzBuzzer();
assertEquals("Fizz", sut.convert(input));
}
Code language: JavaScript (javascript)
Notice that I added more input values than the ones I used before. Since it’s so easy, nothing is stopping me from adding as many values as I feel necessary.
Now, the same for the multiples of five:
@ParameterizedTest
@ValueSource(ints = {5, 10, 20, 25, 35, 40, 50 })
public void convert_multiplesOfFiveResultsInBuzz(int input) {
var sut = new FizzBuzzer();
assertEquals("Buzz", sut.convert(input));
}
Code language: JavaScript (javascript)
And then, the multiples of fifteen:
@ParameterizedTest
@ValueSource(ints = {15, 30, 45, 60, 75, 90 })
public void convert_multiplesOfFifteenResultsInBuzz(int input) {
var sut = new FizzBuzzer();
assertEquals("FizzBuzz", sut.convert(input));
}
Code language: Java (java)
Now we need to cover the scenario of numbers that aren’t multiples of either three or five. In those cases, the numbers themselves should be returned.
@ParameterizedTest
@ValueSource(ints = {1, 2, 4, 7, 8, 11, 13, 14})
public void convert_otherNumbersResultInTheNumbersThemselves(int input) {
var sut = new FizzBuzzer();
assertEquals(Integer.toString(input), sut.convert(input));
}
Code language: Java (java)
Now that we have parameterized all the tests, don’t forget to delete the old ones! The updated test class should look like this:
@ParameterizedTest
@ValueSource(ints = {0, -1, 1001})
public void convert_invalidInputResultsInNonApplicableResponse(int input) {
var sut = new FizzBuzzer();
assertEquals("N/A", sut.convert(input));
}
@ParameterizedTest
@ValueSource(ints = {3, 6, 9, 12, 33, 36, 39, 42})
public void convert_multiplesOfThreeResultsInFizz(int input) {
var sut = new FizzBuzzer();
assertEquals("Fizz", sut.convert(input));
}
@ParameterizedTest
@ValueSource(ints = {5, 10, 20, 25, 35, 40, 50 })
public void convert_multiplesOfFiveResultsInBuzz(int input) {
var sut = new FizzBuzzer();
assertEquals("Buzz", sut.convert(input));
}
@ParameterizedTest
@ValueSource(ints = {15, 30, 45, 60, 75, 90 })
public void convert_multiplesOfFifteenResultsInBuzz(int input) {
var sut = new FizzBuzzer();
assertEquals("FizzBuzz", sut.convert(input));
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 4, 7, 8, 11, 13, 14})
public void convert_otherNumbersResultInTheNumbersThemselves(int input) {
var sut = new FizzBuzzer();
assertEquals(Integer.toString(input), sut.convert(input));
}
Code language: JavaScript (javascript)
JUnit parameterized tests: A step beyond
You might look at the tests above and think there’s quite a lot of repetition. Though I don’t disagree, I’d leave them as they are because each test clearly represents a specific scenario.
But let’s say you wanted to remove the duplication from the tests above, keeping a single test for all scenarios. You may think you could write a test like this:
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5}, strings = {"1", "2", "Fizz", "4", "Buzz"})
public void convert(int input, String expectedResult) {
var sut = new FizzBuzzer();
assertEquals(expectedResult, sut.convert(input));
}
Code language: Java (java)
However that test wouldn’t work, because the @ValueSource
annotation doesn’t support passing more than one parameter. Luckily, there’s a way out. First, add the following two imports:
private static Stream<Arguments> inputsAndResults() {
return Stream.of(
Arguments.of(-1, "N/A"),
Arguments.of(0, "N/A"),
Arguments.of(1001, "N/A"),
Arguments.of(1, "1"),
Arguments.of(2, "2"),
Arguments.of(3, "Fizz"),
Arguments.of(4, "4"),
Arguments.of(5, "Buzz"),
Arguments.of(6, "Fizz"),
Arguments.of(10, "Buzz"),
Arguments.of(15, "FizzBuzz"),
Arguments.of(30, "FizzBuzz"),
Arguments.of(33, "Fizz"),
Arguments.of(45, "FizzBuzz")
);
}
Code language: Java (java)
The method above will act as a source of data for the new test method you’ll create. Finally, add the test method:
@ParameterizedTest
@MethodSource("inputsAndResults")
public void convert(int input, String expectedResult) {
var sut = new FizzBuzzer();
assertEquals(expectedResult, sut.convert(input));
}
Code language: Java (java)
It’s the same test from before, the one I said wouldn’t work. But now, instead of the @ValueSource
annotation, it uses @MethodSource
. As the argument for the annotation, we pass the method’s name created in the previous step – and now it works.For each pair of arguments, JUnit will generate and execute a dedicated test.
JUnit parameterized tests: A path to cleaner test code
Your programming language of choice can be Java, JavaScript, Python, or any other, it makes no difference: unit testing is essential. And unit test code is code like any other; any strategies to make it cleaner, simpler, easier to navigate, and easier to maintain are welcome.
How about you try out JUnit in the sandbox below?
Conclusion
In this post, you’ve learned how to write a JUnit parameterized test. As you’ve seen, it’s easy to get started with, but the benefits are invaluable. By parameterizing tests, you remove code duplication, make the maintenance of test code easier, and reduce the complexity of test methods, improving their reliability.
What should your next steps be? As it turns out, software testing is a huge field with plenty to learn. Here’s a suggestion: read up on the difference between unit and integration tests and the role that each plays in a well-balanced software quality strategy. Thanks for reading!
This post was written by Carlos Schults. Carlos is a consultant and software engineer with experience in desktop, web, and mobile development. Though his primary language is C#, he has experience with a number of languages and platforms. His main interests include automated testing, version control, and code quality.