Search code examples
mavengroovyjunit4mojogmaven

Unit Testing GMavenPlus Groovy Mojos - project.basedir not being expanded


I am currently attempting to write a Maven plugin using GMavenPlus (thank you @Keegan!) and Groovy 2.4.3. In a nutshell, the plugin parses a directory of SQL DDLs and generates output from those parsed DDLs

The Mojo itself works just fine when built, imported, and run within a full consuming project. Horrah!

The problem is with unit testing. When trying to unit test this Mojo, Maven POM vars like ${project.basedir} are not being expanded and thus the mojo is failing with an error like "File not found! [${project.basedir}/src/test/resources/ddl]". As you can see from that error message, ${project.basedir} was passed as a literal instead of being expanded.

I am currently using the Maven Plugin Testing Harness (with the fixed dependencies, see this blog), JUnit 4.12, and AssertJ 3.0.0 as my testing stack.

Any ideas or specific tricks to get things like project.basedir to expand in a unit test?

Thanks in advance!

Unit Test in Question:

import edge.SqlToScalaMojo
import org.junit.Before
import org.junit.Test

/**
 * Created by medge on 6/15/15.
 */
class SqlToScalaMojoTest extends BaseMojoTest<SqlToScalaMojo> {

    SqlToScalaMojo mojo

    @Before
    void setup() {
        mojo = getMojo("parse-ddls")
    }

    @Test
    void testMojoExecution() throws Exception {
        assertThat mojo isNotNull()

        mojo.execute()
    }
}

BaseMojoTest.groovy (really just a convenience base class):

import org.apache.maven.plugin.AbstractMojo
import org.apache.maven.plugin.testing.MojoRule
import org.junit.Rule

/**
 * Base Test class for Mojo tests. Extends {@link org.assertj.core.api.Assertions}
 *
 * If a type is given to this class then the result of #getMojo() does not have to be cast, reducing the amount of code
 * to be written in the unit tests themselves.
 *
 * Created by medge on 6/5/15.
 */
abstract class BaseMojoTest<T extends AbstractMojo> extends org.assertj.core.api.Assertions {

    /**
     * MojoRule used to lookup Mojos
     */
    @Rule public MojoRule rule = new MojoRule()

    /**
     * Get a configured mojo using the default pom file. Calls #getMojo(goal, getPom()) implicitly
     *
     * @param goal Goal to look up
     * @return T configured Mojo
     */
    T getMojo(String goal) {
        getMojo(goal, getPom())
    }

    /**
     * Get a configured mojo using the specified pom file
     *
     * @param goal Goal to look up
     * @param pom POM file to use when configuring Mojo
     * @return T configured Mojo
     */
    T getMojo(String goal, File pom) {
        T mojo = (T) rule.lookupMojo(goal, pom)

        mojo
    }

    /**
     * Default POM file if no custom path is given
     */
    String defaultPomPath = "src/test/resources/plugin-config.xml"

    /**
     * Return a File reference containing the default POM file
     *
     * @return File
     */
    File getPom() {
        getPom(defaultPomPath)
    }

    /**
     * Return a File reference containing the POM file found at the specified path. Implicitly asserts that the POM
     * exists using <code>assertFile</code>
     *
     * @param path Path to user-defined POM (overrides the default if provided)
     * @return File containing the specified POM.
     */
    File getPom(String path) {
        File _pom = getTestFile(path)

        // Implicitly assert POM exists
        assertFile(_pom)

        // Then return the POM file
        _pom
    }

    /**
     * Convenience method to assert that a file is valid
     *
     * @param file File to validate
     */
    static void assertFile(File file) {
        assertThat file isNotNull()
        assertThat file exists()
    }

    /**
     * Get the current project's base directory. From {@link org.codehaus.plexus.PlexusTestCase}
     *
     * @return Base directory path
     */
    static String getBaseDir() {
        final String path = System.getProperty( "basedir" );

        path ?: new File( "" ).getAbsolutePath();
    }

    /**
     * Return a test file from the src/test/resources directory. Assumes the base directory is src/test/resources so the
     * src/test/resources prefix can be omitted from the path if desired
     *
     * @param path File path
     * @return File
     */
    static File getTestFile(String path) {
        File testFile

        if(path.indexOf("src/test/resources/") > -1)
            testFile = getTestFile(getBaseDir(), path)
        else
            testFile = getTestFile(getBaseDir(), "src/test/resources/${path}")

        testFile
    }

    /**
     * Retrieve a test file from the given baseDir/path
     *
     * @param baseDir String base directory to look in
     * @param path String path to the file desired
     * @return File
     */
    static File getTestFile(String baseDir, String path) {
        new File(baseDir, path)
    }
}

Main POM file for the Mojo itself:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>edge</groupId>
    <artifactId>parser-mojo</artifactId>
    <version>0.0.3-SNAPSHOT</version>
    <packaging>maven-plugin</packaging>

    <properties>
        <groovy.version>2.4.3</groovy.version>
        <maven.version>3.3.3</maven.version>
        <junit.version>4.12</junit.version>
        <assertj.version>3.0.0</assertj.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>${groovy.version}</version>
        </dependency>

        <!-- Test dependencies -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj.version}</version>
            <scope>test</scope>
        </dependency>


        <!-- Dependencies for Maven Mojos -->
        <dependency>
            <groupId>org.codehaus.plexus</groupId>
            <artifactId>plexus-utils</artifactId>
            <version>3.0.22</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-core</artifactId>
            <version>${maven.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-artifact</artifactId>
            <version>${maven.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-compat</artifactId>
            <version>${maven.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>${maven.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-testing</groupId>
            <artifactId>maven-plugin-testing-harness</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
            <type>jar</type>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>addSources</goal>
                            <goal>addTestSources</goal>
                            <goal>generateStubs</goal>
                            <goal>compile</goal>
                            <goal>testGenerateStubs</goal>
                            <goal>testCompile</goal>
                            <goal>removeStubs</goal>
                            <goal>removeTestStubs</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.4</version>
                <configuration>
                    <!-- see http://jira.codehaus.org/browse/MNG-5346 -->
                    <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
                </configuration>
                <executions>
                    <execution>
                        <id>generate-descriptor</id>
                        <goals>
                            <goal>descriptor</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>help-goal</id>
                        <goals>
                            <goal>helpmojo</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Test POM used during the unit test:

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>edge</groupId>
                <artifactId>parser-mojo</artifactId>
                <version>0.0.3-SNAPSHOT</version>
                <configuration>
                    <template>${project.basedir}/src/test/resources/sample.template</template>
                    <inputDir>${project.basedir}/src/test/resources/ddl</inputDir>
                    <outputDir>${project.basedir}/src/test/resources/generated/</outputDir>
                </configuration>
                <executions>
                    <execution>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>parse-ddls</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Thanks!


Solution

  • Found the answer to my own question. Posting here in case anybody else is looking for the same issue.

    First, I was running into another issue with the Mojo itself in regards to the @Parameter annotation. The following:

    @Parameter(defaultValue = "${project.basedir}/src/main/resources")
    String inputDir
    

    Will generate an error because the Groovy compiler will pick up the string "${project.basedir}/src/main/resources" before Maven. It will then do its evaluation and convert it to a GString. This generates an error because the annotation expects a java.lang.String but it will get java.lang.Object

    The solution, as pointed out by @Keegan in another question, is to use single quotes instead of double quotes:

    @Parameter(defaultValue = '${project.basedir}/src/main/resources')
    String inputDir
    

    The Groovy compiler won't evaluate the string and Maven will pick up from there

    Next, the original question about defaultValue's not being read during the unit test. The culprit was this method in BaseMojoTest.groovy:

    T getMojo(String goal, File pom) {
        T mojo = (T) rule.lookupMojo(goal, pom)
    
        mojo
    }
    

    Specifically, this part:

    rule.lookupMojo(goal, pom)
    

    lookupMojo on a MojoRule does not evaluate Parameter annotation's default value. It expects all possible parameters are present in the test POM file. A quick dive into the source for MojoRule and I found a way to fix it:

    /**
     * Get a configured mojo using the specified pom file
     *
     * @param goal Goal to look up
     * @param pom POM file to use when configuring Mojo
     * @return T configured Mojo
     */
    T getMojo(String goal, File pom) {
        T mojo = (T) rule.lookupConfiguredMojo(getMavenProject(pom), goal)
    
        mojo
    }
    
    /**
     * Method to handle creating a MavenProject instance to create configured Mojos from
     * @param pom File to POM file containing Mojo config
     * @return MavenProject
     */
    MavenProject getMavenProject(File pom) {
        // create the MavenProject from the pom.xml file
        MavenExecutionRequest request = new DefaultMavenExecutionRequest()
        ProjectBuildingRequest configuration = request.getProjectBuildingRequest()
                .setRepositorySession(new DefaultRepositorySystemSession())
        MavenProject project = rule.lookup(ProjectBuilder.class).build(pom, configuration).getProject()
    
        project.basedir = new File(getBaseDir())
    
        // Implicit assert
        assertThat project isNotNull()
    
        // And return
        project
    }
    

    The getMavenProject() method is a slight variation to the code found in MojoRule#readMavenProject(), altered to reference the MojoRule and my definition of getBaseDir(). The Mojos generated by this method now properly evaluate @Parameter's defaultValue! I do feel like getMavenProject() could probably be made more efficient (instead of creating a MavenProject every time) but this will suffice for now.

    One more thing

    You may have noticed this line from the getMavenProject() method:

    project.basedir = new File(getBaseDir())
    

    This very annoying little hack is necessary because when creating the MavenProject object:

    MavenProject project = rule.lookup(ProjectBuilder.class).build(pom, configuration).getProject()
    

    ${project.basedir} actually becomes the basedir of the test POM file used in the unit test. If, like me, you have a test POM in src/test/resources then basedir will become something like /Users/medge/.../src/test/resources. Ergo, when @Parameter annotations with ${project.basedir} are expanded, such as:

    @Parameter(defaultValue='${project.basedir}/src/main/resources')
    String inputDir
    

    When ran from a unit test, inputDir will resolve to /Users/medge/.../src/test/resources/src/main/resources

    Subtle things can trip you up...

    Hope this is helpful to someone else!