Maven JPMS without test-scoped dependencies

110 views Asked by At

Having a maven project "Test".

This is the pom.xml:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>Test</groupId>
    <artifactId>Test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>LATEST</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

There is a class /src/main/java/test/Test.java like this:

package test;
public class Test {
    public final static String SQL = "CALL sysdate()";
}

And a unit-test /src/test/java/test/unittest/UnitTest.java like this:

package test.unittest;
import java.sql.*;
import org.hsqldb.jdbcDriver;
import test.Test;

public class UnitTest {
    public void testSQL() throws SQLException {
        try (Connection c = jdbcDriver.getConnection("jdbc:hsqldb:mem:test", null)) {
            Statement stmt = c.createStatement();
            if (stmt.execute(Test.SQL)) {
                ResultSet result = stmt.getResultSet();
                while (result.next()) {
                    System.out.println(result.getString(1));
                }
            }
        }
    }
}

If I call mvn install (as you can see) the test is executed.

grim@main:~/workspace/Test$ mvn install
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for Test:Test:jar:0.0.1-SNAPSHOT
[WARNING] 'dependencies.dependency.version' for org.hsqldb:hsqldb:jar is either LATEST or RELEASE (both of them are being deprecated) @ line 16, column 13
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO] 
[INFO] -----------------------------< Test:Test >------------------------------
[INFO] Building Test 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- resources:3.3.0:resources (default-resources) @ Test ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO] 
[INFO] --- compiler:3.10.1:compile (default-compile) @ Test ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- resources:3.3.0:testResources (default-testResources) @ Test ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO] 
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ Test ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- surefire:3.0.0-M8:test (default-test) @ Test ---
[INFO] Using auto detected provider org.apache.maven.surefire.junit.JUnit3Provider
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running test.unittest.UnitTest
2024-02-27 10:31:31
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.119 s - in test.unittest.UnitTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- jar:3.3.0:jar (default-jar) @ Test ---
[INFO] 
[INFO] --- install:3.1.0:install (default-install) @ Test ---
[INFO] Installing /home/grim/workspace/Test/pom.xml to /home/grim/.m2/repository/Test/Test/0.0.1-SNAPSHOT/Test-0.0.1-SNAPSHOT.pom
[INFO] Installing /home/grim/workspace/Test/target/Test-0.0.1-SNAPSHOT.jar to /home/grim/.m2/repository/Test/Test/0.0.1-SNAPSHOT/Test-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.706 s
[INFO] Finished at: 2024-02-27T10:31:31+01:00
[INFO] ------------------------------------------------------------------------
grim@main:~/workspace/Test$ 

That is the setup!

Problem:

In order to use JPMS I need to mention java.sql in the production module /src/main/java/module-info.java like this:

module Test {
  exports test;
  requires java.sql; // <-- unittest-dependencies in production-jar
}

Question

How to avoid test-dependencies in production-jar?

1

There are 1 answers

5
VonC On

You could try and separate the module definitions for your main code and your test code when using the Java Platform Module System (JPMS).
That would allow you to declare requires java.sql; only for your test module, keeping your production JAR free of test dependencies.

Test/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── test/
│   │   │       ├── Test.java
│   │   │       └── module-info.java  (Main module-info)
│   │   └── ...
│   └── test/
│       ├── java/
│       │   ├── test/
│       │   │   └── unittest/
│       │   │       └── UnitTest.java
│       │   └── module-info.java  (Test module-info)
│       └── ...

The /src/main/java/module-info.java should not require java.sql because it is not needed for the main application's modules.

module Test {
    exports test;
}

The /src/test/java/module-info.java would be separate module descriptor for your test code. It requires java.sql because your tests use JDBC.

open module test.unittest {
    requires Test;
    requires java.sql;
    requires org.hsqldb;
}

Make sure your pom.xml is set up to compile the test module-info.java correctly. Maven needs to know that the test code forms a separate module. That typically involves configuring the Maven Compiler Plugin to handle the module path for both the main and test compilation phases. You might need to adjust plugin configurations to make sure your test module-info is recognized and compiled correctly.

For instance:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>Test</groupId>
    <artifactId>Test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Your production dependencies here -->
        <!-- Example test dependency -->
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>LATEST</version> <!-- Consider specifying a fixed version instead -->
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version> <!-- Make sure to use a version that supports JPMS -->
                <configuration>
                    <release>11</release> <!-- Use release instead of source and target to make sure module-info is handled correctly -->
                </configuration>
                <executions>
                    <!-- Reconfiguration for the test-compile phase to include the module path -->
                    <execution>
                        <id>test-compile</id>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <release>11</release>
                            <testModulePath>${project.build.testOutputDirectory}</testModulePath>
                            <modulePaths>
                                <modulePath>
                                    <path>${project.build.directory}</path>
                                </modulePath>
                            </modulePaths>
                            <compilerArgs>
                                <!-- Add module-info.java from test sources -->
                                <arg>--patch-module</arg>
                                <arg>Test=${project.build.testSourceDirectory}</arg>
                            </compilerArgs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

The <release> tag is used instead of <source> and <target> for better handling of JPMS features.
The test compilation phase is specifically configured to recognize the test module. The --patch-module option is used to specify the test module-info.java, allowing it to augment or replace the module declaration for tests.
The test module path is adjusted to make sure the test module-info.java is compiled and recognized as part of the test module.

That setup should allow you to maintain separate module descriptors for your production and test code, avoiding the inclusion of test dependencies in your production artifacts.


Make sure the Maven Surefire Plugin, which runs your tests, is configured to support JPMS. That may involve specifying additional module path entries or adjusting the plugin's configuration to recognize your test module setup.

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- Existing configuration -->

    <build>
        <plugins>
            <!-- Existing plugins configuration -->
            <!-- Configuration for the Maven Compiler Plugin -->
            
            <!-- Configuration for the Maven Surefire Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version> <!-- Make sure compatibility with JPMS -->
                <configuration>
                    <argLine>--add-modules java.sql</argLine>
                    <argLine>--add-reads Test=java.sql</argLine>
                    <argLine>--add-exports java.sql/test=ALL-UNNAMED</argLine>
                    <argLine>--patch-module Test=${project.build.testOutputDirectory}</argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

As noted by the OP Grim in the comments, <useModulePath>true</useModulePath>, which tells Surefire to use the module path instead of the classpath (essential for running tests in a modular environment) is true by default.

The --add-modules, --add-reads, and --add-exports arguments adjust the module graph for the test run, allowing the test module to access required modules and packages.
The --patch-module argument is used to include the test classes and resources in the module being tested. That is necessary because the test classes are not part of the original module definition.


Although, as seen in "How can I add a module to the module path in using the Maven Surefire plugin?", the Maven Surefire Plugin version 3.0.0-M6 and newer automatically adds dependencies with a test scope to the module path. That feature simplifies the inclusion of modules like simplethingprovider at test time without the need for explicit requires statements in the module-info.java of the main module (fancythingprovider in the context of the question).

Make sure the Maven Surefire Plugin is at least version 3.0.0-M6:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version> <!-- Or newer -->
    <configuration>
        <!-- Configuration as necessary -->
    </configuration>
</plugin>

If you wanted to include a hypothetical module, let's call it testsupportmodule (another Maven project that provides additional functionality or mocks specifically for testing purposes), only at test time for the Test project, you could update your pom.xml for the Test project with:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>Test</groupId>
    <artifactId>Test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Existing production dependencies -->
        
        <!-- Test-scoped dependency for the testsupportmodule -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>testsupportmodule</artifactId>
            <version>1.0.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin configuration -->
            
            <!-- Maven Surefire Plugin configuration -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version> <!-- Ensure using a version that supports JPMS correctly -->
                <configuration>
                    <!-- Configuration specific to your testing needs, ensuring module path includes testsupportmodule -->
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

This pom.xml snippet includes the testsupportmodule as a test-scoped dependency. This means it will be available on the classpath during the test compilation and execution phases but won't be included in the production jar file or required by the production module descriptor (module-info.java). That would avoid test dependencies in the production artifact while still enabling comprehensive testing using additional modules that provide mock implementations, helpers, or other test-specific functionality.


From Naman's comments and "Testing In The Modular World":

The --patch-module option is used to add or replace files in a module, which is useful when your test code needs to reside in the same package as the classes under test but in a separate source set (e.g., src/test/java). This approach is particularly beneficial when testing package-private classes or methods.
However, if your tests only need to access public classes and methods and are located in a different package, the need for --patch-module diminishes. The choice to use --patch-module depends on the specific access requirements of your test code to the classes under test.

Given that useModulePath is true by default in recent versions of the Maven Surefire Plugin, Namam's suggestion to set <useModulePath> to false as a workaround would force Maven to use the classpath instead of the module path, essentially bypassing the module system for test execution. This approach can simplify testing scenarios at the cost of not fully leveraging the encapsulation benefits of JPMS during testing.