Java 9 + maven + junit: does test code need module-info.java of its own and where to put it?
The module system does not distinguish between production code and test code, so if you choose to modularize test code, the prod.module
and the test.module
cannot share the same package com.acme.project
, as described in the specs:
Non-interference — The Java compiler, virtual machine, and run-time system must ensure that modules that contain packages of the same name do not interfere with each other. If two distinct modules contain packages of the same name then, from the perspective of each module, all of the types and members in that package are defined only by that module. Code in that package in one module must not be able to access package-private types or members in that package in the other module.
As indicated by Alan Bateman, the Maven compiler plugin uses --patch-module and other options provided by the module system when compiling code in the src/test/java tree, so that the module under test is augmented with the test classes. And this is also done by the Surefire plugin when running the test classes (see Support running unit tests in named Java 9 modules). This means you don't need to place your test code in a module.
You might want to rethink the project design you're trying to implement. Since you are implementing a module and its test into a project, you shall refrain from using different modules for each of them individually.
There should just be one single module-info.java
for a module and its corresponding tests.
Your relevant project structure might look like this:-
Project/
|-- pom.xml/
|
|-- src/
| |-- test/
| | |-- com.acme.project
| | | |-- com/acme/project
| | | | |-- SomeTest.java
| |
| |-- main/
| | |-- com.acme.project
| | | |-- module-info.java
| | | |-- com/acme/project
| | | | |-- Main.java
where the module-info.java
could further be:-
module com.acme.project {
requires module1;
requires module2;
// requires junit; not required using Maven
}
Just to sum all of the above as per your questions --
I feel I follow wrong path, it all starts looking very ugly. How can I have module-info.java of its own in test code, or how do I achieve the same effects (require, etc) without it?
Yes, you should not consider managing different modules for test code making it complex.
You can achieve similar effect by treating junit
as a compile-time dependency using the directives as follows-
requires static junit;
Using Maven you can achieve this following the above-stated structure and using maven-surefire-plugin
which would take care of patching the tests to the module by itself.
I just want to add my 0.02$
here on the general testing approach, since it seems no one is addressing gradle
and we use it.
First thing first, one needs to tell gradle
about modules. It is fairly trivial, via (this will be "on" since gradle-7
):
plugins.withType(JavaPlugin).configureEach {
java {
modularity.inferModulePath = true
}
}
Once you need to test your code, gradle
says this:
If you don’t have a
module-info.java
file in your test source set (src/test/java
) this source set will be considered as traditional Java library during compilation and test runtime.
In plain english, if you do not define a module-info.java
for testing purposes - things "will just work" and in the majority of cases this is exactly what we want.
But, that is not the end of story. What if I do want to define an JUnit5 Extension
, via ServiceLocator
. That means I need to go into module-info.java
, from tests; one that I yet do not have.
And gradle
has that solved again:
Another approach for whitebox testing is to stay in the module world by patching the tests into the module under test. This way, module boundaries stay in place, but the tests themselves become part of the module under test and can then access the module’s internals.
So we define a module-info.java
in src/test/java
, where I can put :
provides org.junit.jupiter.api.extension.Extension with zero.x.extensions.ForAllExtension;
we also need to do --patch-module
, just like maven plugins do it. It looks like this:
def moduleName = "zero.x"
def patchArgs = ["--patch-module", "$moduleName=${tasks.compileJava.destinationDirectory.asFile.get().path}"]
tasks.compileTestJava {
options.compilerArgs += patchArgs
}
tasks.test {
jvmArgs += patchArgs
}
The only problem is that intellij
does not "see" this patch and thinks that we also need a requires
directive (requires zero.x.services
), but that's not really the case. All the tests run just fine from command line and intellij
.
The example is here