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.
As always, the code is available over on Github.
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.