
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: January 16, 2024
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).
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.
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:
@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:
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.
Now that we understand what a ParameterResolver extension is, we’re ready to write:
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:
@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.
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.
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:
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!)
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.