Skip to content

Will SpringExtension keep supporting customization of TestContextManager in JUnit Jupiter 5 ExtensionContext.Store? #35851

@kodstark

Description

@kodstark

I implemented a TemplateSpringExtension which extends SpringExtension to separate Spring context per JUnit Jupiter ClassTemplate parameter and still benefit from cached contexts.

It works because I can update the JUnit Jupiter 5 ExtensionContext.Store with prepared context like below and because SpringExtension uses store.getOrComputeIfAbsent which won't have an effect as TestContextManager is already defined by TemplateSpringExtension.

  private static final ExtensionContext.Namespace TEST_CONTEXT_MANAGER_NAMESPACE =
      ExtensionContext.Namespace.create(SpringExtension.class);

  private void prepareTemplateInvocation(ExtensionContext context) {
    Class<?> testClass = context.getRequiredTestClass();
    ExtensionContext.Store store = context.getRoot().getStore(TEST_CONTEXT_MANAGER_NAMESPACE);
    store.remove(testClass);
    store.put(testClass, new TestContextManager(getBootstraper(testClass)));
  }

Can I assume that it won't break with future Spring updates?

Full code looks like:

public class TemplateSpringExtension extends SpringExtension {

  private final TemplateContext templateContext;

  public record TemplateContext(String displayName, Object param, String... properties) {}

  public interface WithTemplateParameter {
    void withTemplateParameter(Object value);
  }

  public static ClassTemplateInvocationContext getClassTemplateInvocationContext(
      TemplateContext templateContext) {
    return new InvocationContext(templateContext);
  }

  @SuppressWarnings("unused")
  public TemplateSpringExtension() {
    this(null);
  }

  public TemplateSpringExtension(TemplateContext templateContext) {
    this.templateContext = templateContext;
  }

  private static final ExtensionContext.Namespace TEST_CONTEXT_MANAGER_NAMESPACE =
      ExtensionContext.Namespace.create(SpringExtension.class);

  private void prepareTemplateInvocation(ExtensionContext context) {
    Class<?> testClass = context.getRequiredTestClass();
    ExtensionContext.Store store = context.getRoot().getStore(TEST_CONTEXT_MANAGER_NAMESPACE);
    store.remove(testClass);
    store.put(testClass, new TestContextManager(getBootstrapper(testClass)));
  }

  /**
   * Similar to what BootstrapUtils.resolveTestContextBootstrapper(testClass) but with support for
   * class template parameter.
   *
   * <p>Name of {@link TestContextBootstrapper} is part of the {@link MergedContextConfiguration} so
   * We return {@link SpringBootTestContextBootstrapper} used by default by SpringBoot if there are
   * no custom properties.
   *
   * <p>Otherwise, we return bootstrapper with properties customized by test template.
   */
  private TestContextBootstrapper getBootstrapper(Class<?> testClass) {
    DefaultCacheAwareContextLoaderDelegate contextLoader =
        new DefaultCacheAwareContextLoaderDelegate();
    DefaultBootstrapContext bootstrapContext =
        new DefaultBootstrapContext(testClass, contextLoader);
    String[] contextProperties = getContextProperties();
    TestContextBootstrapper bootstrapper =
        contextProperties != null
            ? new Bootstrapper(contextProperties)
            : new SpringBootTestContextBootstrapper();
    bootstrapper.setBootstrapContext(bootstrapContext);
    return bootstrapper;
  }

  private String[] getContextProperties() {
    if (templateContext != null
        && templateContext.properties() != null
        && templateContext.properties().length > 0) {
      return templateContext.properties();
    }
    return null;
  }

  private static class Bootstrapper extends SpringBootTestContextBootstrapper {

    private final String[] contextProperties;

    public Bootstrapper(String... contextProperties) {
      this.contextProperties = contextProperties;
    }

    @Override
    protected String[] getProperties(Class<?> testClass) {
      return contextProperties;
    }
  }

  private static class InvocationContext implements ClassTemplateInvocationContext {
    private final TemplateContext templateContext;
    private final TemplateSpringExtension envExtension;

    private InvocationContext(TemplateContext templateContext) {
      this.templateContext = templateContext;
      this.envExtension = new TemplateSpringExtension(templateContext);
    }

    @Override
    public String getDisplayName(int invocationIndex) {
      return templateContext.displayName();
    }

    @Override
    public void prepareInvocation(ExtensionContext context) {
      envExtension.prepareTemplateInvocation(context);
    }

    @Override
    public List<Extension> getAdditionalExtensions() {
      List<Extension> result = new ArrayList<>();
      result.add(envExtension);
      TestInstancePostProcessor testInstancePostProcessor =
          (testInstance, context) -> {
            if (testInstance instanceof WithTemplateParameter withTestParam) {
              withTestParam.withTemplateParameter(templateContext.param());
            }
          };
      result.add(testInstancePostProcessor);
      return result;
    }
  }
}

Metadata

Metadata

Assignees

Labels

in: testIssues in the test modulestatus: invalidAn issue that we don't feel is valid

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions