Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-48466

Provide JUnit 5 support for JenkinsRule

    XMLWordPrintable

    Details

    • Similar Issues:

      Description

      JUnit 4 rules are not supported by JUnit 5. In order to use JenkinsRule in a test one cannot switch to JUnit 5 for these tests. You still need to have a dependency to JUnit 4.

        Attachments

          Activity

          Hide
          mkobit Mike Kobit added a comment -

          One way to at least make it simpler for plugins to start using JUnit 5 would be to separate some of the specific start/stop details from the apply methods (like in

          https://github.com/jenkinsci/jenkins-test-harness/blob/d7acb8e67e3da73297b6ca1d3c6987d5de0f8e6a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java#L531-L588 ) so that a JUnit 5 extension can easily wrap those calls to the existing test bootstrap logic.

          I don't think it is a huge problem for JUnit 4 to still be present but untangling some of the test bootstrapping code from the JUnit 4 specific rules will enable the use of JUnit 5 (or any other testing framework) from a plugins standpoint.

          Show
          mkobit Mike Kobit added a comment - One way to at least make it simpler for plugins to start using JUnit 5 would be to separate some of the specific start/stop details from the apply methods (like in https://github.com/jenkinsci/jenkins-test-harness/blob/d7acb8e67e3da73297b6ca1d3c6987d5de0f8e6a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java#L531-L588   )  so that a JUnit 5 extension can easily wrap those calls to the existing test bootstrap logic. I don't think it is a huge problem for JUnit 4 to still be present but untangling some of the test bootstrapping code from the JUnit 4 specific rules will enable the use of JUnit 5 (or any other testing framework) from a plugins standpoint.
          Hide
          mkobit Mike Kobit added a comment -

          Another possibility would be to implement ExternalResource to enable usage of https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4-rule-support 

          Show
          mkobit Mike Kobit added a comment - Another possibility would be to implement ExternalResource to enable usage of https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4-rule-support  
          Hide
          chrahunt Chris Hunt added a comment -

          I was able to get something started using a ParameterResolver, like so:

          package io.jenkins.plugins.websub;
          
          import org.junit.jupiter.api.extension.AfterEachCallback;
          import org.junit.jupiter.api.extension.ExtensionContext;
          import org.junit.jupiter.api.extension.ParameterContext;
          import org.junit.jupiter.api.extension.ParameterResolutionException;
          import org.junit.jupiter.api.extension.ParameterResolver;
          import org.jvnet.hudson.test.JenkinsRecipe;
          
          import java.util.Optional;
          
          public class TestUtils {
              public static class JenkinsRule extends org.jvnet.hudson.test.JenkinsRule {
                  private final ParameterContext context;
          
                  JenkinsRule(ParameterContext context) {
                      this.context = context;
                  }
          
                  @Override
                  public void recipe() throws Exception {
                      Optional<JenkinsRecipe> a = context.findAnnotation(JenkinsRecipe.class);
                      if (!a.isPresent()) return;
                      final JenkinsRecipe.Runner runner = a.get().value().newInstance();
                      recipes.add(runner);
                      tearDowns.add(() -> runner.tearDown(this, a.get()));
                  }
              }
          
              public static class JenkinsParameterResolver implements ParameterResolver, AfterEachCallback {
                  private static final String key = "jenkins-instance";
                  private static final ExtensionContext.Namespace ns =
                          ExtensionContext.Namespace.create(JenkinsParameterResolver.class);
          
                  @Override
                  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
                          throws ParameterResolutionException {
                      return parameterContext.getParameter().getType().equals(JenkinsRule.class);
                  }
          
                  @Override
                  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
                          throws ParameterResolutionException {
                      JenkinsRule instance = extensionContext.getStore(ns).getOrComputeIfAbsent(
                              key, key -> new JenkinsRule(parameterContext), JenkinsRule.class);
                      try {
                          instance.before();
                          return instance;
                      } catch (Throwable t) {
                          throw new ParameterResolutionException(t.toString());
                      }
                  }
          
                  @Override
                  public void afterEach(ExtensionContext context) throws Exception {
                      JenkinsRule rule = context.getStore(ns).remove(key, JenkinsRule.class);
                      if (rule != null)
                          rule.after();
                  }
              }
          }
          

          Used like

          package io.jenkins.plugins.websub;
          
          import hudson.model.FreeStyleBuild;
          import hudson.model.FreeStyleProject;
          import hudson.tasks.Shell;
          import org.apache.commons.io.FileUtils;
          import org.junit.jupiter.api.Test;
          import org.junit.jupiter.api.extension.ExtendWith;
          
          @ExtendWith(TestUtils.JenkinsParameterResolver.class)
          public class TestWebSubTrigger {
              @Test
              void testJenkinsParameterResolver(TestUtils.JenkinsRule j) throws Exception {
                  FreeStyleProject project = j.createFreeStyleProject();
                  project.getBuildersList().add(new Shell("echo hello"));
                  FreeStyleBuild build = project.scheduleBuild2(0).get();
                  System.out.println(build.getDisplayName() + " completed");
                  String s = FileUtils.readFileToString(build.getLogFile());
              }
          }
          

          I'm not sure if/where this kind of approach would fit in jenkins-test-harness. Oleg Nenashev, what do you think?

          Show
          chrahunt Chris Hunt added a comment - I was able to get something started using a ParameterResolver , like so: package io.jenkins.plugins.websub; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.jvnet.hudson.test.JenkinsRecipe; import java.util.Optional; public class TestUtils { public static class JenkinsRule extends org.jvnet.hudson.test.JenkinsRule { private final ParameterContext context; JenkinsRule(ParameterContext context) { this .context = context; } @Override public void recipe() throws Exception { Optional<JenkinsRecipe> a = context.findAnnotation(JenkinsRecipe.class); if (!a.isPresent()) return ; final JenkinsRecipe.Runner runner = a.get().value().newInstance(); recipes.add(runner); tearDowns.add(() -> runner.tearDown( this , a.get())); } } public static class JenkinsParameterResolver implements ParameterResolver, AfterEachCallback { private static final String key = "jenkins-instance" ; private static final ExtensionContext.Namespace ns = ExtensionContext.Namespace.create(JenkinsParameterResolver.class); @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return parameterContext.getParameter().getType().equals(JenkinsRule.class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { JenkinsRule instance = extensionContext.getStore(ns).getOrComputeIfAbsent( key, key -> new JenkinsRule(parameterContext), JenkinsRule.class); try { instance.before(); return instance; } catch (Throwable t) { throw new ParameterResolutionException(t.toString()); } } @Override public void afterEach(ExtensionContext context) throws Exception { JenkinsRule rule = context.getStore(ns).remove(key, JenkinsRule.class); if (rule != null ) rule.after(); } } } Used like package io.jenkins.plugins.websub; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.tasks.Shell; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(TestUtils.JenkinsParameterResolver.class) public class TestWebSubTrigger { @Test void testJenkinsParameterResolver(TestUtils.JenkinsRule j) throws Exception { FreeStyleProject project = j.createFreeStyleProject(); project.getBuildersList().add( new Shell( "echo hello" )); FreeStyleBuild build = project.scheduleBuild2(0).get(); System .out.println(build.getDisplayName() + " completed" ); String s = FileUtils.readFileToString(build.getLogFile()); } } I'm not sure if/where this kind of approach would fit in jenkins-test-harness. Oleg Nenashev , what do you think?
          Hide
          t8ch Thomas Weißschuh added a comment -

          To get it to work with TestExtension I had to modify the JenkinsRule from Chris Hunt:

                JenkinsRule(ParameterContext context,
                    ExtensionContext extensionContext) {
                    this.context = context;
                    this.testDescription = Description.createTestDescription(
                      extensionContext.getTestClass().map(Class::getName).orElse(null),
                      extensionContext.getTestMethod().map(Method::getName).orElse(null)
                    );
                  }
          

          To make it work with @RegisterExtension (to interact with the rule from @BeforeEach methods) the following works:

          public static class JenkinsExtension extends org.jvnet.hudson.test.JenkinsRule implements BeforeEachCallback, AfterEachCallback {
          
              @Override
              public void beforeEach(ExtensionContext context) throws Exception {
                this.testDescription = Description.createTestDescription(
                  context.getTestClass().map(Class::getName).orElse(null),
                  context.getTestMethod().map(Method::getName).orElse(null)
                );
                try {
                  before();
                } catch (Throwable throwable) {
                  throw new Exception(throwable);
                }
              }
          
              @Override
              public void afterEach(ExtensionContext context) throws Exception {
                after();
              }
          
              @Override
              public void recipe() throws Exception {
              }
          
          Show
          t8ch Thomas Weißschuh added a comment - To get it to work with TestExtension I had to modify the JenkinsRule from Chris Hunt : JenkinsRule(ParameterContext context, ExtensionContext extensionContext) { this .context = context; this .testDescription = Description.createTestDescription( extensionContext.getTestClass().map( Class ::getName).orElse( null ), extensionContext.getTestMethod().map(Method::getName).orElse( null ) ); } To make it work with @RegisterExtension (to interact with the rule from @BeforeEach methods) the following works: public static class JenkinsExtension extends org.jvnet.hudson.test.JenkinsRule implements BeforeEachCallback, AfterEachCallback { @Override public void beforeEach(ExtensionContext context) throws Exception { this .testDescription = Description.createTestDescription( context.getTestClass().map( Class ::getName).orElse( null ), context.getTestMethod().map(Method::getName).orElse( null ) ); try { before(); } catch (Throwable throwable) { throw new Exception(throwable); } } @Override public void afterEach(ExtensionContext context) throws Exception { after(); } @Override public void recipe() throws Exception { }
          Hide
          drulli Ulli Hafner added a comment -

          Has someone the spare time to provide these ideas as a PR for the test harness?

          Show
          drulli Ulli Hafner added a comment - Has someone the spare time to provide these ideas as a PR for the test harness?

            People

            • Assignee:
              olivergondza Oliver Gondža
              Reporter:
              drulli Ulli Hafner
            • Votes:
              5 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

              • Created:
                Updated: