This post has originally been published on the codecentric blog. It was translated to english and slightly edited for this blog.
A month back the JUnit team published the offical version of JUnit 5. There’s plenty of resources for high level overviews and if you want to catch up I can recommend this (german) article on the codecentric blog by my colleague Tobias Trelle. In this post I want to have a closer look at how JUnit 5’s test engine Jupiter enables Parameterized Tests. To fully appreciate the new possibilities, let’s have a brief look what we had to do up until now:
JUnit 4 contained a test runner named Parameterized
, which would take parameters from a method annotated with @Parameters
and use them, when creating instances of a test class. You could then use those constructor parameters from your tests. Looking at the example above you probably can imagine, that these test quickly became confusing and hard to maintain. On top of that most of the time it didn’t make much sense to have more than one test method in every parameterized test class.
Other alternatives like JUnitParams took a different approach and made those tests a bit easier by moving the definition of parameters nearer to the respective test. Still, those tests frequently were harder to grasp then necessary:
Both approaches are based on the usage of test runners. Since we could only specify one test runner for every test class there was no way to combine functionality from different test runners. For example, if we wanted to use the HierarchicalContextRunner to structure our parameterized tests hierarchically we found ourselves between a rock and a hard spot.
Parameterized tests with JUnit 5
Using Jupiter you can approach parameterized tests in two different ways: Dynamic Tests
were introduced pretty early in the development phase and they are one way to create parameterized tests. Additionally there’s a feature that’s actually called Parameterized Tests
, it appeared later with milestone 4.
Dynamic Tests
Generally, we specify our tests statically. We implement a test method and annotate it with @Test
. JUnit then discovers and executes those tests. Dynamic tests work differently:
In contrast to static tests we have to implement a method that returns a collection, an iterable or a stream of type DynamicTest
. This method has to be annotated with @TestFactory
. You can use a static method called dynamicTest()
to create a dynamic test instance using a display name (see @DisplayName) as its first parameter and a lambda containing the test code to be executed as its second parameter. A @TestFactory
method can also return DynamicContainer
collections as a dynamic equivalent of a static test class.
One way to use dynamic tests would be to have logic in the factory method that decides upfront if certain tests are to be generated or not. In comparison, static tests always exist at runtime and you would have to use Assumptions or Execution conditions to abort test execution.
You can also use dynamic tests to create tests from a data source, though. I imagine this could be an option if a lot of logic would be involved (e.g. downloading a file provided by a business unit and transforming the contained data before creating tests from it).
Parameterized tests
However, it’s easier to create parameterized tests using the annotation @ParameterizedTest
. You’ll use it instead of @Test
und it has to be accompanied by a second annotation that connects the source of the parameters, an ArgumentsProvider
:
The values returned by an ArgumentsProvider
have to match the type of the method parameter. Jupiter comes with a set of predefined ArgumentProviders
with their respective annotations: In the example above I’m using @ValueSource
, you can use its attributes to define static values that should be passed to your test method. @CsvSource
and @CsvFileSource
enable the use of csv style data and @EnumSource
passes the values of an enumeration to a test. With @MethodSource
you can connect an arbitrary (static) method as a source for test parameters.
Implementing a custom ArgumentsProvider
This concept of parameterized tests is similar to the JUnitParams approach. Still it’s a lot more flexible only looking at the different providers that Jupiter has on board. It gets even more interesting because you can implement your own ArgumentsProvider
. For example, an implementation that enables the use of Json data could look like this:
Using the annotation @JsonSource
we can define an array of string values, each containing some json data. The type
attribute is used to define a target type for deserialisation. Using the @ArgumentsSource
annotation an @ArgumentsProvider
is connected:
The @AnnotationConsumer
interface requires us to implement the accept()
method, we use it to access the attribute values of the @JsonSource
annotation and copy thos to the provider instance. The provideArguments()
method is implemented for the ArgumentsProvider
interface, it deserializes the value
s to the target type using the Gson library. In our tests, we can now use our annotation as usual:
Conclusion
Many projects can benefit from parameterized tests. The JUnit 4 facilities have been inflexible and resulting tests have been hard to maintain. The JUnit 5 Jupiter test engine brings many new possibilities. For most cases, @ParameterizedTest
will be an easy way to implement our parameterized tests and thanks to the completely new extension model we can also realize specialized use cases like nested, parameterized tests. For complex scenarios dynamic tests might be a reasonable alternative.
The source code examples from this article are available on Github, for further information I recommend the extensive official documentation of the project. JUnit 5 has just been published and I’m eager to see how it will resonate in our day-to-day projects.