Master the most popular testing framework for Java, through the Learn JUnit course:
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
Prior to JUnit 5, to introduce a cool new feature, the JUnit team would have to do it to the core API. With JUnit 5 the team decided it was time to push the capability to extend the core JUnit API outside of JUnit itself, a core JUnit 5 philosophy called βprefer extension points over featuresβ.
In this article, weβre going to focus on one of those extension point interfaces β ParameterResolver β that you can use to inject parameters into your test methods. There are a couple of different ways to make the JUnit platform aware of your extension (a process known as βregistrationβ), and in this article, weβll focus on declarative registration (i.e., registration via source code).
2. ParameterResolver
Injecting parameters into your test methods could be done using the JUnit 4 API, but it was fairly limited. With JUnit 5, the Jupiter API can be extended β by implementing ParameterResolver β to serve up objects of any type to your test methods. Letβs have a look.
2.1. FooParameterResolver
public class FooParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Foo.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return new Foo();
}
}
First, we need to implement ParameterResolver β which has two methods:
- supportsParameter() β returns true if the parameterβs type is supported (Foo in this example), and
- resolveParamater() β serves up an object of the correct type (a new Foo instance in this example), which will then be injected in your test method
2.2. FooTest
@ExtendWith(FooParameterResolver.class)
public class FooTest {
@Test
public void testIt(Foo fooInstance) {
// TEST CODE GOES HERE
}
}
Then to use the extension, we need to declare it β i.e., tell the JUnit platform about it β via the @ExtendWith annotation (Line 1).
When the JUnit platform runs your unit test, it will get a Foo instance from FooParameterResolver and pass it to the testIt() method (Line 4).
The extension has a scope of influence, which activates the extension, depending on where itβs declared.
The extension may either be active at the:
- method level, where it is active for just that method, or
- class level, where it is active for the entire test class, or @Nested test class as weβll soon see
Note: you should not declare a ParameterResolver at both scopes for the same parameter type, or the JUnit Platform will complain about this ambiguity.
For this article, weβll see how to write and use two extensions to inject Person objects: one that injects βgoodβ data (called ValidPersonParameterResolver) and one that injects βbadβ data (InvalidPersonParameterResolver). Weβll use this data to unit test a class called PersonValidator, which validates the state of a Person object.
3. Write the Extensions
Now that we understand what a ParameterResolver extension is, weβre ready to write:
- one which provides valid Person objects (ValidPersonParameterResolver), and
- one which provides invalid Person objects (InvalidPersonParameterResolver)
3.1. ValidPersonParameterResolver
public class ValidPersonParameterResolver implements ParameterResolver {
public static Person[] VALID_PERSONS = {
new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
new Person().setId(2L).setLastName("Baker").setFirstName("James"),
new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
new Person().setId(5L).setLastName("English").setFirstName("Jane"),
new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
};
Notice the VALID_PERSONS array of Person objects. This is the repository of valid Person objects from which one will be chosen at random each time the resolveParameter() method is called by the JUnit platform.
Having the valid Person objects here accomplishes two things:
- Separation of concerns between the unit test and the data that drives it
- Reuse, should other unit tests require valid Person objects to drive them
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
If the type of parameter is Person, then the extension tells the JUnit platform that it supports that parameter type, otherwise it returns false, saying it does not.
Why should this matter? While the examples in this article are simple, in a real-world application, unit test classes can be very large and complex, with many test methods that take different parameter types. The JUnit platform must check with all registered ParameterResolvers when resolving parameters within the current scope of influence.
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
}
return ret;
}
A random Person object is returned from the VALID_PERSONS array. Note how resolveParameter() is only called by the JUnit platform if supportsParameter() returns true.
3.2. InvalidPersonParameterResolver
public class InvalidPersonParameterResolver implements ParameterResolver {
public static Person[] INVALID_PERSONS = {
new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
new Person().setId(2L).setLastName(",Baker").setFirstName(""),
new Person().setId(3L).setLastName(null).setFirstName(null),
new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
};
Notice the INVALID_PERSONS array of Person objects. Just like with ValidPersonParameterResolver, this class contains a store of βbadβ (i.e., invalid) data for use by unit tests to ensure, for example, that PersonValidator.ValidationExceptions are properly thrown in the presence of invalid data:
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
}
return ret;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
The rest of this class naturally behaves exactly like its βgoodβ counterpart.
4. Declare and Use the Extensions
Now that we have two ParameterResolvers, itβs time to put them to use. Letβs create a JUnit test class for PersonValidator called PersonValidatorTest.
Weβll be using several features available only in JUnit Jupiter:
- @DisplayName β this is the name that shows up on test reports, and much more human readable
- @Nested β creates a nested test class, complete with its own test lifecycle, separate from its parent class
- @RepeatedTest β the test is repeated the number of times specified by the value attribute (10 in each example)
By using @Nested classes, weβre able to test both valid and invalid data in the same test class, while at the same time keeping them completely sandboxed away from each other:
@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {
@Nested
@DisplayName("When using Valid data")
@ExtendWith(ValidPersonParameterResolver.class)
public class ValidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are valid")
public void validateFirstName(Person person) {
try {
assertTrue(PersonValidator.validateFirstName(person));
} catch (PersonValidator.ValidationException e) {
fail("Exception not expected: " + e.getLocalizedMessage());
}
}
}
@Nested
@DisplayName("When using Invalid data")
@ExtendWith(InvalidPersonParameterResolver.class)
public class InvalidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are invalid")
public void validateFirstName(Person person) {
assertThrows(
PersonValidator.ValidationException.class,
() -> PersonValidator.validateFirstName(person));
}
}
}
Notice how weβre able to use the ValidPersonParameterResolver and InvalidPersonParameterResolver extensions within the same main test class β by declaring them only at the @Nested class level. Try that with JUnit 4! (Spoiler alert: you canβt do it!)
5. Conclusion
In this article, we explored how to write two ParameterResolver extensions β to serve up valid and invalid objects. Then we had a look at how to use these two ParameterResolver implementations in a unit test.
And, if you want to learn more about the JUnit Jupiter extension model, check out the JUnit 5 Userβs Guide, or part 2 of my tutorial on developerWorks.
