This article is part of my blog series on automated testing promoting my new Pluralsight course Effective Automated Testing with Spring.

The JUnit team continues to make great progress on adding new features and enhancements to the JUnit 5 framework. We already have a second significant feature update after just seven months from the initial release of JUnit 5.

In this article, we look at some of the key features and enhancements added in JUnit 5.2 which was released on April 29th.

Build Tool Enhancements

There were a number of important enhancements made to build tool support in JUnit 5.2 that resolve annoyances as well as help in the adoption/migration to JUnit 5 for existing test suites. Let’s take a look.

New BOM to Ease Dependency Management Concerns

One irritation with JUnit 5’s switch to a more modular architecture is that it can be time-consuming to keep dependencies properly in-sync. You are no longer managing a single dependency like with JUnit 3 or 4 but multiple dependencies.

To address this concern, the JUnit team created a BOM to make the task of version management more simple.

To make use of JUnit’s new BOM, you will need to configure your dependency management to point to it.

<dependencyManagement> <dependencies> <dependency> <groupId>org.junit</groupId> <artifactId>junit-bom</artifactId> <version>5.2.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

Note: In my demo code, things don’t quite work this way. In my demo, the parent is spring-boot-starter-parent version 2.1.0.BUILD-SNAPSHOT which, as of this writing, is still pointing to JUnit 5.1.1. In Maven and Gradle, when there is a conflict between which dependency version to use, the one in the parent wins over the imported BOM. Though that too is being overwritten as I overwrote the properties Spring-Boot uses to define which version of JUnit 5 dependencies it is using. Yes I know, confusing!

Surefire Improvements and Support for JDK 9/10

JUnit 5.2 offers support for Surefire 2.21.0. This will, in turn, allow JUnit 5 tests to be executed in Java 9 and 10 environments. JUnit 5 skipped supporting Surefire 2.20 due to memory leak problems and issues executing tests in Java 9.

Another benefit that comes with support for Surefire 2.21.0 is support for passing in null/blank properties. You can see some background on this issue in a previous blog post. Instead of having to pass a no-op value like “none” like I did in my previous article, now you can simply leave the properties blank/undefined. To me, it makes it more clear that when running the nightly-build no tests should be excluded.

<profiles> <profile> <id>ci-build</id> <properties> <excludeTags>integration</excludeTags> </properties> </profile> <profile> <id>nightly-build</id> </profile> </profiles>

Failsafe Support

I’m really happy to see that with JUnit 5.2 the failsafe-plugin is now supported. Previously, I would use profiles to support integration tests (like in the above example) so that my integration tests were not executing with every build.

While not needed with the integration tests I run*, the failsafe-plugin offers some additional lifecycle steps that allow for the setting-up and tearing-down that is sometimes needed when executing integration tests. The configuration for the failsafe-plugin is very similar to that of the surefire-plugin. Note it is not a typo that I am using the surefire-provider in the dependencies.

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.21.0</version> <dependencies> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-surefire-provider</artifactId> <version>${junit-platform.version}</version> </dependency> <!-- Vintage engine only needed if JUnit 3/4 tests present --> <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>${junit-jupiter.version}</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit-jupiter.version}</version> </dependency> </dependencies> </plugin>

*Self-promotion: I will be speaking at a number of conferences on how to design portable and reliable integration tests so that advanced set up and tear down steps are not necessary. I would recommend attending if you are able and have been struggling when writing integration tests, or just want to learn about some of the new tools and techniques.

Further Enhancements to Parameterized Tests

Parameterized tests continue to be a major focus of attention in JUnit 5. If you want to get up-to-speed on all the changes made to Parameterized test in JUnit 5, you can check out my presentation Welcome to JUnit 5. I also previously cover the enhancements made to parameterized tests in JUnit 5.1 here.

External @MethodSource

Previously when using the @MethodSource , the supplying static factory method had to be located within the same class as the @ParameterizedTest that was being executed. With JUnit 5.2, the @MethodSource can now be located in an external class. This could be particularly helpful when you have a large number of scenarios you want to execute, or to generally help with test class readability and separation of concerns. To call an external static factory method, you must follow this pattern: path.to.the.Class#methodName . Here is an example:

@ParameterizedTest @MethodSource("com.bk.hotel.service.impl.ParameterizedTestData#data") public void verifyDateValidation(DateValidationBean dateValidation) { ReservationServiceImpl service = new ReservationServiceImpl(); List<String> errorMsgs = service.verifyReservationDates(dateValidation.checkInDate, dateValidation.checkOutDate); assertThat(errorMsgs).containsExactlyInAnyOrder(dateValidation.errorMsgs); }

ArgumentsAccessor for @CsvSource

The @CsvSource received a lot of improvements in JUnit 5.2 with the new ArgumentsAccessor.

Previously, the method signature for a test case that used the @CsvSource had to match the shape of the record passed in. This causes some maintenance headaches if the format of records changes and readability concerns when having to deal with really long method signatures. Additionally, the composition of the test object needed to happen in every test case which added to the complexity of tests.

Let’s take a look at how the new ArgumentAccessor improves on the experience of writing parameterized tests with the @CsvSource , as well as some of the refinements that can be made.

Below is the most basic usage of the ArgumentAccessor here instead of adding an argument to the method signature for every field. Instead we can just use ArgumentAccessor . Within the test case, we then define how the ArgumentAccessor should make use of each field.

There is a bit of complexity in how I am setting up the DateValidationBean as I am making use of the ... operator in my constructor which means some records will have error messages while others don’t.

@ParameterizedTest @CsvSource({ "Valid booking dates, 03/03/2020, 03/07/2020", "Null check-in date, , 11/27/2020, Must provide a check-in date.", "Both dates null, , , 'Must provide a check-in date., Must provide a check-out date.'", "Invalid check-in date, 02/30/2020, 03/07/2020, check-in date of: 02/30/2020 is not a valid date or does not match date format of: MM/DD/YYYY" }) public void verifyDateValidationUsingArgumentAccessor(ArgumentsAccessor arguments) { DateValidationBean dateValidation; try { dateValidation = new DateValidationBean(arguments.getString(0), arguments.getString(1), arguments.getString(2), StringArrayConverter.convert(arguments.get(3))); } catch (PreconditionViolationException exception) { // Used when scenario has no messages being returned dateValidation = new DateValidationBean(arguments.getString(0), arguments.getString(1), arguments.getString(2)); } ReservationServiceImpl service = new ReservationServiceImpl(); List<String> errorMsgs = service.verifyReservationDates(dateValidation.checkInDate, dateValidation.checkOutDate); assertThat(errorMsgs).containsExactlyInAnyOrder(dateValidation.errorMsgs); }

It’s a bit distracting that most of the test case is devoted to setting up the DateValidationBean .

Let’s cleanly extract-out the composition of the DateValidationBean from the test case. This can be done in two easy steps. First, implement the ArgumentsAggregator interface. The interface has one method, aggregateArguments , where I essentially can copy-in the code from my test case:

public class DateValidationBeanAggregator implements ArgumentsAggregator { @Override public DateValidationBean aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { DateValidationBean dateValidation; try { dateValidation = new DateValidationBean(arguments.getString(0), // arguments.getString(1), // arguments.getString(2), // StringArrayConverter.convert(arguments.get(3))); } catch (PreconditionViolationException exception) { // Used when scenario has no messages being returned dateValidation = new DateValidationBean(arguments.getString(0), // arguments.getString(1), // arguments.getString(2)); } return dateValidation; } }

Next, tell JUnit to use DateValidationBeanAggregator when running a test. I need to update the method signature to look like this:

public void verifyDateValidationUsingConverter( @AggregateWith(DateValidationBeanAggregator.class) DateValidationBean dateValidation)

If I am using the DateValidationBean in a lot of test cases, or I just wanted to shorten the method signature for my test cases, I might consider creating a custom annotation as well. This would like this:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @AggregateWith(DateValidationBeanAggregator.class) public @interface ToDataValidationBean { }

And now to use DateValidationBeanAggregator I need only add my custom annotation @ToDataValidationBean :

public void verifyDateValidationUsingAnnotation(@ToDataValidationBean DateValidationBean dateValidation)

Conclusion

It’s good to see so much active development for the JUnit framework. JUnit 5 was released last September and we are already two minor releases in – that is an impressive cadence from the JUnit team!

To see all the changes in JUnit 5.2 be sure to check the release notes. Looking at the backlog for JUnit 5, we should still have much to look forward as well. If you haven’t started transitioning to JUnit 5 yet, the enhancements in 5.2 are yet more reason to do so.

If you are interested in learning more about JUnit 5, check out the user guides. You can also catch me presenting on JUnit 5, for times and locations check here: https://billykorando.com/presentation-schedule/.

The demo code used in this blog post can be found here.

Automated Testing Series