Inject Parameters into JUnit Jupiter Unit Tests

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:

  1. Separation of concerns between the unit test and the data that drives it
  2. 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.

Related posts:

Logout in an OAuth Secured Application
Java Program to Find the Shortest Path from Source Vertex to All Other Vertices in Linear Time
Biến trong java
Java Program to Check whether Directed Graph is Connected using DFS
Java Program to Implement Shell Sort
Registration – Activate a New Account by Email
Java Program to Encode a Message Using Playfair Cipher
Remove HTML tags from a file to extract only the TEXT
Guide to @ConfigurationProperties in Spring Boot
Java – File to Reader
Java Program to Check if a Directed Graph is a Tree or Not Using DFS
Prevent Cross-Site Scripting (XSS) in a Spring Application
DistinctBy in the Java Stream API
Hướng dẫn sử dụng luồng vào ra nhị phân trong Java
ClassNotFoundException vs NoClassDefFoundError
Introduction to Using FreeMarker in Spring MVC
Arrays.asList vs new ArrayList(Arrays.asList())
How to Break from Java Stream forEach
Java Program to Implement Cartesian Tree
The Registration Process With Spring Security
Java Program to Implement LinkedBlockingQueue API
Spring Data MongoDB – Indexes, Annotations and Converters
Java Program to Implement Stack API
Chuyển đổi giữa các kiểu dữ liệu trong Java
Java Program to Check Whether a Directed Graph Contains a Eulerian Path
Removing all Nulls from a List in Java
Java Program to Check if a Given Graph Contain Hamiltonian Cycle or Not
RegEx for matching Date Pattern in Java
Spring Webflux and CORS
Java Program to implement Circular Buffer
Java Web Services – JAX-WS – SOAP
Quick Intro to Spring Cloud Configuration