Skip to content

Support dynamic tests with JUnit5/Spock/TestNG/... ("maven-plugin-testing-harness"-like tests) #490

Open
@Djaytan

Description

@Djaytan

Hello,

I explored this new suggested solution for testing Maven plugins but then I realized it is unable to answer my (specific?) needs (from what I understood so far).

In fact, what I want is very simple: being able to dynamically generate Maven projects (in my case by providing an instance of MavenProject built from a Model). The main goal sought here is to follow DRY principle by avoiding duplicating pom.xml files again and again with only a very small difference not always easy to spot most of the time. Well, with ITF and maven-invoker-plugin, when the need is to only write few tests then that may be ok... But what about the case where you want to write dozens or maybe even hundreds of tests?

The maven-plugin-testing-harness framework allows me to do that. ITF not (except if I'm wrong?).

Here is an example of (more or less generic) test that I have:

public final class MyMojoTest extends AbstractMojoTestCase {

  private final FileSystem imfs = Jimfs.newFileSystem(Configuration.unix());

  // Not used here, but used for making the assertions in fact
  private final Path rootDir = imfs.getPath(".");

  @Override
  protected void tearDown() throws Exception {
    super.tearDown();
    imfs.close();
  }

  public void test_execute_nominalCase() {
    // Assemble
    TestConfig testConfig = NOMINAL_TEST_CONFIG;
    MavenProject mavenProject = generateMavenProject(testConfig);
    createInputFiles(testConfigs);

    // Act
    executeMyPlugin(mavenProject);

    // Assert
    assertGeneratedOutputFiles(testConfig);
  }

  // And then we can imagine lots of other tests here...

  // ...

  private void executeMyPlugin(@NotNull MavenProject mavenProject) {
    try {
      MyMojo mojo =
          (MyMojo) lookupConfiguredMojo(mavenProject, "my-mojo");
      mojo.injectCustomFileSystem(imfs);
      mojo.execute();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

Note: the logic behind the generateMavenProject() method implementation may be not trivial at all depending on how complex the plugin's configuration model is. However, the base implementation of the method is the following one:

public final class MavenProjectGenerator {

  public static @NotNull MavenProject generateMavenProject(
      @Nullable Collection<TestConfig> configs) {
    Model model = new Model();
    model.setModelVersion("4.0.0");
    model.setGroupId("my.group.id");
    model.setArtifactId("a-test-project");
    model.setVersion("0.0.0-TEST");

    Build build = new Build();
    build.addPlugin(generateMyPluginModel(configs));
    model.setBuild(build);

    var mavenProject = new MavenProject(model);
    LOG.info("Generated POM content:\n{}", convertToXmlContent(mavenProject));

    return mavenProject;
  }

  private static @NotNull Plugin generateMyPluginModel(
      @Nullable Collection<TestConfig> configs) {
    Plugin plugin = new Plugin();
    plugin.setGroupId("my.group.id");
    plugin.setArtifactId("my-test-plugin");
    plugin.setConfiguration(generatePluginConfiguration(configs));
    return plugin;
  }

  // And we build the configuration part of the plugin by relying on the Xpp3Dom class...
}

Based on this example, what I would expect from a modern Maven test framework is mainly just to have the possibility to rely on regular JUnit 5 tests with standard annotations like @Test, @ParameterizedTest and so on.
So this means: no abstract class like AbstractMojoTestCase to extend, just a call like... Let's say randomly MojoTestExecutor#execute(MavenProject, String) and that's all! I really think this is achievable.

So this would lead to the following result:

import org.junit.jupiter.api.AutoClose;
import org.junit.jupiter.api.Test;

// We no longer have to extend/implement any class/interface
final class MyMojoTest {

  /*
   * Since JUnit Jupiter v5.11 we can rely on @AutoClose annotation
   * By default, the #close() method is called
   * See: https://junit.org/junit5/docs/current/user-guide/#writing-tests-built-in-extensions-AutoClose
   */
  @AutoClose private final MojoTestExecutor mojoTestExecutor = new MojoTestExecutor();

  @AutoClose private final FileSystem imfs = Jimfs.newFileSystem(Configuration.unix());

  // Not used here, but used for making the assertions in fact
  private final Path rootDir = imfs.getPath(".");

  // Nothing changed here
  public void test_execute_nominalCase() {
    // Assemble
    TestConfig testConfig = NOMINAL_TEST_CONFIG;
    MavenProject mavenProject = generateMavenProject(testConfig);
    createInputFiles(testConfigs);

    // Act
    executeMyPlugin(mavenProject);

    // Assert
    assertGeneratedOutputFiles(testConfig);
  }

  // A lot more simpler!
  private void executeMyPlugin(@NotNull MavenProject mavenProject) {
    /* 
     * Here the proposed method signature is just a straight to the goal solution
     * for sharing my idea without worrying about the details.
     * The last parameter is supposed to permit injection of custom dependencies
     * in the IoC container such as Guice, Spring, Quarkus, ...
     * Here, I'm injecting an in-memory file system for improving tests performances,
     * avoiding polluting the laptop FS in case of clean up failure
     * and ensuring portability across platforms (Windows, Linux, MacOS, ...)
     */
    mojoTestExecutor.execute(mavenProject, "my-mojo", imfs);
  }
}

But maybe what should be done is simply make some evolutions on the maven-plugin-testing-harness framework itself? But in this case then we should consider the fact ITF is only a better alternative to the maven-invoker-plugin.

What do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions