How to pass parameterized test parameters to BeforeEach/AfterEach method in Junit5

How to pass parameterized test parameters to BeforeEach/AfterEach method in Junit5

·

8 min read

Some time ago, when I was implementing some performance test I faced the issue with parameterized tests and BeforeEach/AfterEach methods. I tried to test some scenario against few implementations of Pulsar functions to compare their performance. My idea was to pass configuration needed to run each implementation as a parameterized test parameters. Naturally, I assumed that BeforeEach/AfterEach methods are the best places to prepare and tear down the environment for the test. I tried just to specify that arguments in the method signature but it turned out that ParameterizedTestParameterResolver doesn't resolve parameters for other methods than methods annotated with ParameterizedTest annotation. I started googling and I found few questions about that on the forums but without any solution, so I decided to take a look and try to do that by my own. I achieved that through to Junit5 Extentions and I would like to share my approach to that with you.

First, what we need to do is implementing org.junit.jupiter.api.extension.ParameterResolver interface. The ParameterizedTestParameterResolver is one of its implementation which is registered for the test annotated with ParameterizedTest annotation. JUnit uses registered implementations of ParameterResolver interface to resolve parameters for the test constructor and any other method annotated with one of the following annotations :

  • org.junit.jupiter.api.Test
  • org.junit.jupiter.api.BeforeEach
  • org.junit.jupiter.api.AfterEach
  • org.junit.jupiter.api.BeforeAll
  • org.junit.jupiter.api.AfterAll

The interface defines two methods which we need to override.

The first of them is the supportsParameter method. That's the way how the parameter resolver can define if it's able to resolve requested parameter for the specific method. If none of the registered parameters resolver return true for some parameter, exception will be thrown. JUnit will complain also when more than one of parameters resolvers will declare support for the same parameter. In our solution we want to resolve parameters only for methods annotated with BeforeEach/AfterEach annotations, so we need to check if executed method is annotated with one of them. When we are sure that we are asked about the parameter for the proper method we need to ensure also if it's supported by the ParameterizedTestParameterResolver.

We will start with checking if our parameter resolver is asked for the parameters for proper method.

    private boolean isExecutedOnAfterOrBeforeMethod(ParameterContext parameterContext) {
        return Arrays.stream(parameterContext.getDeclaringExecutable().getDeclaredAnnotations())
                .anyMatch(this::isAfterEachOrBeforeEachAnnotation);
    }

    private boolean isAfterEachOrBeforeEachAnnotation(Annotation annotation) {
        return annotation.annotationType() == BeforeEach.class || annotation.annotationType() == AfterEach.class;
    }

Now we can ask ParameterizedTestParameterResolver (implementation of ParameterResolver interface registered under the hood by JUnit for parameterized test) if it supports requested parameter by invoking supportsParameter on that resolver. We need to pass two arguments to that method (ParameterContext, and ExtensionContext). The ParameterContext describes requested parameter, it brings information about parameters index in the method declaration, target and the standard Parameter from reflection api. ParameterizedTestParameterResolver resolves only parameters of test method annotated with ParameterizedTest annotation, so it will verify if the parameter from ParameterContext is a parameter of test function by checking executable property for the first argument. Parameter from our parameterContext points to argument from our BeforeEach/AfterEach method, so we need to cheat a little bit. We need to map our parameterContext to parameterContext which will point to test method argument. It's possible to access that parameter by invoking extensionContext.getRequiredTestMethod().getParameters() method.

private MappedParameterContext getMappedContext(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return new MappedParameterContext(
                parameterContext.getIndex(),
                extensionContext.getRequiredTestMethod().getParameters()[parameterContext.getIndex()],
                Optional.of(parameterContext.getTarget()));
    }

We assumed that our method annotated with BeforeEach/AfterEach annotation arguments has the same declaration as the test method(The same list of arguments).

MappedParameterContext is our implementation of ParameterContext interface required by supportsParameter method.

@AllArgsConstructor
@Getter
public class MappedParameterContext implements ParameterContext {

    private final int index;
    private final Parameter parameter;
    private final Optional<Object> target;

    @Override
    public boolean isAnnotated(Class<? extends Annotation> annotationType) {
        return AnnotationUtils.isAnnotated(parameter, annotationType);
    }

    @Override
    public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType) {
        return AnnotationUtils.findAnnotation(parameter, annotationType);
    }

    @Override
    public <A extends Annotation> List<A> findRepeatableAnnotations(Class<A> annotationType) {
        return AnnotationUtils.findRepeatableAnnotations(parameter, annotationType);
    }
}

As a second argument we can pass extensionContext from our supportsParameter method.

Now we have everything what we need to implement supportsParameter method.

@Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        if (isExecutedOnAfterOrBeforeMethod(parameterContext)) {
            ParameterContext pContext = getMappedContext(parameterContext, extensionContext);
            return parameterisedTestParameterResolver.supportsParameter(pContext, extensionContext);
        }
        return false;
    }

The second method of the ParameterResolver interface which we need to implement is a resolveParameter method. That method declares exactly the same list of arguments. In our implementation we will just delegate method invocation to ParameterisedTestParameterResolver with the mapped parameter context as a first argument.

@Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterisedTestParameterResolver.resolveParameter(getMappedContext(parameterContext, extensionContext), extensionContext);
    }

We referred to the ParameterisedTestParameterResolver instance few times but how we can gain it? To do that we need to implement also BeforeEachMethodAdapter interface. There is one method to implement invokeBeforeEachMethod. It accepts two argument, and the ExtensionRegistry instance is one of them. That's what we need to access to the registered ParameterisedTestParameterResolver.

@Override
    public void invokeBeforeEachMethod(ExtensionContext context, ExtensionRegistry registry) {
        Optional<ParameterResolver> resolverOptional = registry.getExtensions(ParameterResolver.class)
                .stream()
                .filter(parameterResolver -> parameterResolver.getClass().getName().contains("ParameterizedTestParameterResolver"))
                .findFirst();
        if (!resolverOptional.isPresent()) {
            throw new IllegalStateException("ParameterizedTestParameterResolver missed in the registry. Probably it's not a Parameterized Test");
        } else {
            parameterisedTestParameterResolver = resolverOptional.get();
        }
    }

That's all. Now we can register our extension with ExtendWith annotation.

@ExtendWith(AfterBeforeParameterResolver.class)
class AfterBeforeParameterResolverTest {

    private TestEnum capturedParameter;

    @BeforeEach
    public void init(TestEnum parameter) {
        capturedParameter = parameter;
    }

    @ParameterizedTest
    @EnumSource(TestEnum.class)
    public void test(TestEnum parameter) {
        Assertions.assertThat(parameter).isEqualTo(capturedParameter);
    }

    enum TestEnum {
        PARAMETER_1,
        PARAMETER_2;
    }
}

Of course it's a naive implementation. We assumed that our BeforeEach/AfterEach annotated methods have the same list of arguments, in the same order as a test method. To make it more useful we should handle different order of arguments or make it possible to mark some argument as optional. Here is the link to the github where the whole code is pushed: github.com/lamektomasz/AfterBeforeParameter..

My idea is to develop that extension as a library in the free time, so keep fingers crossed :)

I hope that You enjoy this post and feel free to comment.