junit5junit5-extension-model

How to programmatically register extensions in Junit5


Say, a test needs a parameter that is only known when the tests are about to run.

@ExtendWith(MyParameterExtension.class)
public class Test {

  protected final MyParameter p;

  public Test(MyParameter p) {}

  @Test
  public void test() { assertSuccess(TestedCode.doComplexThing(p)); }
}

Only before the tests are executed, the specific contents of MyParameter instance can be determined. So I can have a resolver extension that simple pastes that parameter value where needed:

class MyParameterExtension implements ParameterResolver {

    private final MyParameter myParameter;
 
    public MyParameterExtension(MyParameter p) {
       myParameter = p;
    }

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

    @Override
    public MyParameter resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return myParameter;
    }

}

I run the tests by starting Junit5 from my own code. That's when I can determine what the corresponding parameter values are. Let's say these parameters drive the behavior of the tests, and a user can specify (i.e., over a CLI) the values that a run should use.

How do I register the extension with the test run, as I'm about to commence it?

   void launchSuite(List<DiscoverySelector> selectors, Object something) {
        // The input to this are all the necessary selectors.
        LauncherDiscoveryRequest ldr = LauncherDiscoveryRequestBuilder.request()
           .selectors(selectors).build();
        Launcher launcher = LauncherFactory.create();
        TestPlan plan = launcher.discover(ldr);
        MyParameter myParameter = new MyParameter(something);
        MyParameterExtension ext = new MyParameterExtension(myParameter);
        // $TODO: how do I register my extension with the test run
        // before starting it?
        launcher.execute(plan);
   }

Auto-registering extensions doesn't help me (how would that process know the value of MyParameter) Using @RegisterExtension in the test code doesn't help me (A static block in the test code won't know the proper input for constructing instances of MyParameter)

Looking at the mechanics of launching the test, I don't see anything that lets me register those extensions in advance.

I considered using a ThreadLocal field in an extension registered statically but AFAIU, this won't (reliably) work because JUnit may create its own threads at least in certain cases.

I considered sticking the value of MyParameter in the "extension context", but I don't see a way to grab a hold of that before the test execution starts either. The root context is created in JupiterEngineDescriptor that is, if nothing else, all internal API.

The obvious solution is to stick the parameter in a static field somewhere, but that would preclude me from running tests with different parameters in parallel, unless I resort to loading tests into isolated class loaders, which sounds too cumbersome for something that I believe should be simpler. After all, all of the contexts of a test run are otherwise fully isolated.

What I'm ultimately trying to do, at then, as to make something like this possible:

  // ...
  new Thread(()->launchSuite(selectors, "assume Earth gravity")).start();
  new Thread(()->launchSuite(selectors, "assume Mars gravity")).start();

So what's are the reasonable ways to wire something this together?


Solution

  • Let's start with the one thing that does not work: Using the launcher API. The launcher API is a platform feature, whereas extensions are Jupiter-related. That's why there is no mechanism to register an extension in the API.

    What should work, though, is @RegisterExtension - although you claim it would not. As the documentation shows it is not restricted to static fields. Therefore, whatever you do here:

    MyParameter myParameter = new MyParameter(something);
    MyParameterExtension ext = new MyParameterExtension(myParameter);
    

    could be done in a static method to instantiate an extension during runtime:

    public class Test {
    
      private static MyParameterExtension createExtension() {
        MyParameter myParameter = new MyParameter(something);
        return new MyParameterExtension(myParameter);
      }
    
      @RegisterExtension
      private MyParameterExtension my = createExtension();
    
      @Test
      public void test(MyParameter p) { 
         assertSuccess(TestedCode.doComplexThing(p)); 
      }
    }
    

    If that doesn't work in your case, some information is missing from your problem statement IMO.

    Update

    If your extension creation code requires parameters that can only be determined at launch time, you have the option of adding configuration parameters to the discovery request:

    LauncherDiscoveryRequest ldr = LauncherDiscoveryRequestBuilder.request()
               .configurationParameter("selectors", "assume Earth gravity")
               .selectors(selectors).build();
    

    This parameter can then be retrieved within the extension:

    class MyParameterExtension implements ParameterResolver {
        ...
        @Override
        public MyParameter resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
            var selectors = extensionContext.getConfigurationParameter("selectors").orElse("");
            return new MyParameter(selectors);
        }
    }