Skip to content

Optionally provide test-scoped ExtensionContext for test-specific callbacks in extensions #3445

@JojOatXGME

Description

@JojOatXGME

I am currently writing a small extension which implements ParameterResolver to provide a few parameters which need to be shut down after the test. After reading the documentation, my first intuition was to write the extension like this:

final class MyExtension implements ParameterResolver {
    private static final Namespace NAMESPACE = Namespace.create(MyExtension.class);

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == Parameter.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return extensionContext
                .getStore(NAMESPACE)
                .getOrComputeIfAbsent(ParameterWrapper.class)
                .parameter;
    }

    private static final class ParameterWrapper implements CloseableResource {
        private final Parameter parameter;

        private ParameterWrapper() {
            this.parameter = new Parameter();
        }

        @Override
        public void close() {
            parameter.close();
        }
    }
}

The problem with that solution is that the ExtensionContext is scoped for the whole test class when resolving arguments for the test class constructor, although the test uses TestInstance.Lifecycle.PER_METHOD. As a consequence, my example above will use the same parameter for all tests, instead of creating a new one per test. The documentation does not describe a lot of details about the lifecycle of each ExtensionContext, therefore I cannot declare it as a bug, but this behavior is very unexpected in my opinion. Since the callbacks are on the level of the individual tests, I expected the ExtensionContext to be scoped to the test as well. The same problem applies to TestInstancePreConstructCallback. I haven't tested TestInstanceFactory, but I guess it is also affected.

The obvious workaround is to also add a TestInstancePreDestroyCallback which closes the parameter and removes it from the store. Note that the implementation of that method is not trivial because in contrast to the methods above, this callback receives a child store which is scoped to the test. Unfortunately, this means that calling remove on the given store directly instead of iterating over the parents would not work.

    @Override
    public void preDestroyTestInstance(ExtensionContext context) {
        ParameterWrapper wrapper = null;
        do {
            wrapper = context.getStore(NAMESPACE).remove(ParameterWrapper.class, ParameterWrapper.class);
            context = context.getParent().orElse(null);
        } while (context != null && wrapper == null);
        if (wrapper != null) wrapper.close();
    }

Deliverables

  • Test-scoped ExtensionContext for InvocationInterceptor.interceptTestClassConstructor(…)
  • Test-scoped ExtensionContext for ParameterResolver while resolving constructor parameters
  • Test-scoped ExtensionContext for TestInstancePreConstructCallback
  • Test-scoped ExtensionContext for TestInstancePostProcessor
  • Test-scoped ExtensionContext for TestInstanceFactory
  • Consider Expose test method to test instance constructor extensions #3670 in the implementation

-- or --

  • Documentation of the limitation in the Javadoc of the interfaces or callbacks.

Related issues

Metadata

Metadata

Assignees

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions