Modular projects

The Maven 3 way to make a modular project is to put a module-info.java file in the root directory of Java source files. Because the compilation and execution of tests usually require an amended version of module information, Maven 3 allows to overwrite that file with another module-info.java file placed in the test source directory. While this approach is still supported in Maven 4 for compatibility reasons, it is deprecated and may no longer be supported in a future version. Developers are encouraged to migrate to the approach described below.

Maven 3

The directory layout of a modular project in Maven 3 was as below:

src
├─ main
│  └─ java
│     ├─ module-info.java
│     └─ org/foo/bar/*.java
├─ test
│  └─ java
│     ├─ module-info.java     (optional)
│     └─ org/foo/bar/*.java
└─ target
   └─ classes
      └─ org/foo/bar/*.class

An alternative to the test/java/module-info.java file is to declare compiler arguments such as --add-reads in the <testCompilerArgs> element of the plugin configuration.

Maven 4 with package hierarchy

Maven 4 allows the same directory layout as Maven 3. However, the module-info.java file in the test directory should be replaced by a module-info-patch.maven file in the same directory.

src
├─ main
│  └─ java
│     ├─ module-info.java
│     └─ org/foo/bar/*.java
├─ test
│  └─ java
│     ├─ module-info-patch.maven   (optional)
│     └─ org/foo/bar/*.java
└─ target
   └─ classes
      └─ org/foo/bar/*.class

The Maven compiler automatically adds --patch-module, --add-modules and --add-reads arguments for compiling the tests. If more --add-reads arguments are needed, or if --add-modules, --add-exports or --add-opens arguments are also needed, then a module-info-patch.maven file (syntax described below) can be placed in the test/java directory. This Maven file is preferred to a module-info.java file in the test directory because the Maven file completes the main module-info.class (using compiler arguments) instead of replacing it.

Limitation

When using the package hierarchy, problems may occur if the module name is a single name without . separator (for example, foo or bar but not foo.bar) and that name is identical to a package name. In such case, the hack implemented in the Maven compiler plugin for Maven 3 compatibility become confused about whether a directory named foo represents the module or the package. For avoiding ambiguity, use module names containing at least one . character (as it should be when using the reverse domain name convention) or use the module source hierarchy described below.

Maven 4 with module source hierarchy

The module source hierarchy introduces one additional directory level in the paths to source Java files and to compiled classes. The name of this directory is the Java module name, and the directory is always present even in projects containing only one module. More than one Java module can be present in the same Maven sub-project. Such multi-module projects have advantages such as resolving compiler warnings in forward references to dependent modules and easier sharing of test code between modules. For example, a Maven project for a single Java module named org.foo.bar would have the following directory layout:

src
├─ org.foo.bar
│  ├─ main
│  │  └─ java
│  │     ├─ module-info.java
│  │     └─ org/foo/bar/*.java
│  └─ test
│     └─ java
│        ├─ module-info-patch.maven   (optional)
│        └─ org/foo/bar/*.java
└─ target
   └─ classes
      └─ org.foo.bar
         └─ org/foo/bar/*.class

Note that the output directory also contains an org.foo.bar directory level. That directory level is generated by javac, this is not a convention invented by Maven.

Above layout can be declared with the following fragment in the pom.xml file. Since this example uses the default directory layout for modular projects, the <directory> elements do not need to be specified.

<build>
  <sources>
    <source>
      <module>org.foo.bar</module>
    </source>
    <source>
      <scope>test</scope>
      <module>org.foo.bar</module>
    </source>
  </sources>
</build>

Black Box testing

“Black Box testing” refers to tests executed without access to the internal code of the project to test. Internal codes include package-private classes, interfaces, methods and fields, and also all non-exported packages. Because the module source hierarchy allows any number of Java modules in the same Maven sub-project, it is easy to add an org.foo.bar.test module which will test the org.foo.bar module as if it was an ordinary client application.

White Box testing

“White Box testing” refers to tests which have an access to the internal classes of the project to test. For any <source> element with the test scope, all Java code placed in the directory managed by that element is automatically white box testing for the module declared in the <module> child element. Access to package-private types and members is granted by placing the code in the same package as the code to test. Access to non-exported modules is implicit, but only for the module where the tests belong.

Reusing test fixtures of another module

The Maven 3 way (test-jar) is still supported in Maven 4. However, when using module source hierarchy, it is easier to place test fixtures in the test code of any module which is required by all modules that need these fixtures. The test fixtures can be in any package, not necessarily a package that exists in the main code. Then, the module-info-patch.maven file can export that package to the other modules. For example if the test fixtures are placed in the org.foo.bar.test package of the org.foo.bar module:

patch-module org.foo.bar {            // Put here the name of the module to patch.
    add-modules TEST-MODULE-PATH;     // Recommended value in the majority of cases.
    add-reads TEST-MODULE-PATH;

    add-exports org.foo.bar.test      // The package that contains the test fixtures.
             to SUBPROJECT-MODULES;   // The other modules which want to use those test fixtures.
}

SUBPROJECT-MODULES is a Maven-specific keyword for exporting to all other Java modules in the Maven (sub)project being compiled. It can be replaced by an explicit list of modules. That's all, no need to deploy or install a test JAR.