Boosting your Java application with JUnit Parameterized Tests
As of JUnit 4 we can use Parameterized Tests to reduce (or even to completely remove) test code redundancy. Okay, but what exactly is this feature?
According to Baeldung, this feature "enables us to execute a single test method multiple times with different parameters".
By the end of this article we'll have gone through some useful ways that you can apply into your daily test routine (And by saying this I assume you're aware of the importance of thoroughly testing your code) with a few usage examples I'll be showing you.
(There's also a bonus in the end of the article)
1 — Getting Started
Prior knowledge (basics) in Spring boot, Java and JUnit is required. Yeah, you actually don't need to be a ninja in order to use this type of test.
That's what I'm using right now:
- Spring Boot 2.5.3
- Maven
- JUnit 5
- Java 8+
1.1 — Adding the JUnit dependency
After you've set up your project (I recommend visiting spring initializr website for this), add the following into the pom.xml.
The first dependency not only includes JUnit, but also a couple of libraries that you might need to test your applications (mockito and hamcrest). We'll be using the second one to get argument providers (don't worry, we'll see this later on)
2 — Establishing our scenario
Now it’s time to get our hands dirty!
2.1 — Using a single argument
Supposing we have a function that verify whether a given age is older than 21.
Having done that, we then create a test in order to make sure the exception will be thrown under different arguments.
We must then use the @ParameterizedTest and @ValueSource annotations to tell JUnit we are passing different arguments. This annotation allow us to use a wide variety of arguments (shorts, bytes, ints, longs, floats, doubles, chars, booleans, strings and classes)
We've provided 4 arguments, which means the test will run 4 times. So, on the first iteration, the age variable will assume the value of 10, then 0 and so on.
Once the tests are done, we can easily see the result of each iteration.
2.2— Using multiple arguments
Let's just create a basic function that multiply two integers.
public int calculateTotalPrice(int productPrice, int quantity) {
return productPrice * quantity;
}
Supposing you want to test your function by passing two arguments with the value of 2. The multiplication must be qual to 4. No secrets here.
Ok, there's no problem at all on the test we just did. But there's something you have to keep in mind, though.
As a good developer you know that this very test doesn't encompasses all the possibilities and you certainly want to test different parameters to make sure your code works properly under any circumstances, which in this case could be a negative number or a 0, for instance.
So that's when parameterized test with multiple arguments come into play. Let's see the code first.
What's happening here is: We're using the @CsvSource alongside with @ParameterizedTest.
The first iteration will use the arguments "2, 3, 6", which are bound to the method's signature by simply declaring it.
One important thing is: the arguments are bound in the exact same order you provided. That's how it's going to look like:
- productPrice = 2
- quantity = 3
- totalExpected = 6
I guess you're probably thinking "how come I'm providing a String when the calculatedTotalPrice method requires int arguments?". The answer is that JUnit takes care of this for us.
This is something called Argument Conversion. Although that subject is beyond the scope of this article, I think it is worth mentioning it.
As a matter of curiosity, there's a quick glimpse of what this conversion can do for you:
Yeah, I know… it's quite impressive. It automatically converts the string into a LocalDate. I strongly recommend checking the documentation out for more implicit conversion formats.
2.3 — Using a CSV file
On the previous example we used @CsvSource even though we actually provided a String instead. But this might be a problem if you have loads of data and/or you wish to test with many different scenarios. It can visually become a mess and make your test less readable.
But don't worry, there's a way to use a proper csv file.
Firstly, create a file called testData.csv under src/test/resources path and add the following:
2, 3, 6
0, 10, 0
-5, 8, -40
3, -10, -30
-3, -13, 39
10, 15, 150
9, 9, 81
Okay, we're using the @CsvFileSource annotation, which has a "resources" argument which should be provided with the csv file path (if you file is under src/test/resources you only need to provide the filename with the file extension, since this is the standard root).
2.4 — Enum Source
The annotation @EnumSource allow us to provide all the elements of a enum to test any business logic you need.
2.5 — Method Source
Sometimes you wish to use a set of parameters in more than one test. That might be a good opportunity to use the @MethodSource annotation.
It consists of using an external function that works like an argument provider. You can then easily share your set of parameter with multiple tests.
Pay close attention to the fact that the function dataProvider can be written in any package you need as long as you provide the entire path.
All you need to do is to declare the package where the function is, use a hashtag (#) and the function name as the argument of the annotation.
3 — Bonus ❤
As I promised, here a bonus for you ❤
It is possible to customize the display name of your tests. It probably didn't sound clear enough, but that's fine, let me show you what I mean.
Let's remember this test case:
When you run the test, that's what the display looks like.
You can use specific placeholders to make it visually better:
- {index} — It represents the iteration itself (it starts at 1)
- {0} , {1} …— It represents the variable name. It's used when you have multiple parameters.
The only thing you need to do is to provide the argument name inside the @ParameterizedTest.
Thank you for reading this article. I truly hope you have enjoyed and learnt something new today!