Make the standalone jar be usable with "import module"
JUnit 5 is properly fitted with modules but junit-platform-console-standalone-1.13.4.jar does not include a module-info.class. This is probably due to Java's limitation of not allowing more than one such file in a .jar file, however, it would be reasonable and feasible to have just one: that of the API.
All of the following code assumes Java 25 (or at least Java 23+).
Ideal
The ideal, reasonable-looking minimal tester file for me would look like this.
import module org.junit.jupiter.api;
@Test
void test() {
assertEquals(4, 2+2);
}
It would be compiled as javac -p junit-platform-console-standalone-VSN.jar MinimalTest.java and then run as java -jar junit5_modules.jar execute -cp . -c MinimalTest.
What works in a mock implementation
Due to limitations, the file needs some boilerplate which makes it more annoying for beginners to use.
import module org.junit.jupiter.api;
// bloat: modules provide imports, just not static ones
import static org.junit.jupiter.api.Assertions.*;
@Test
void test() {
assertEquals(4, 2+2);
}
// bloat: compact source files must have this
// or it could be a regular source file with a different sort of bloat
void main() {}
Compilation also requires an extra option: javac --add-modules org.junit.jupiter.api -p junit-platform-console-standalone-VSN.jar MinimalTest.java
The module-info.class file also has to be inserted into junit-platform-console-standalone-VSN.jar (considered as a zip).
Deliverables
- The
junit-platform-console-standalone-***.jarbecomes usable as aorg.junit.jupiter.apimodule.
Slight improvement on the idea: it's even better if the org.junit.jupiter module is used as it also requires org.junit.jupiter.params.
The implementation might require some inspection: as there cannot be other modules inside the jar, the transitively exported contents from the other modules might have to be identified, either in an automated way (which is more error-proof) or manually.
Related to:
- #4079
The standalone variant was never meant to be usable on the module path without friction - as it merges a bunch of JUnit-related API and internals(!) into a single JAR file. Just as JUnit 3 and 4 were shipped and lead to hard-to-manage situations.
Since 5.13.0, and still with the upcoming 6 release, the standalone artifact does provide a stable module name in its manifest, namely org.junit.platform.console.standalone. This helps at least in some situations where the standalone JAR lands on the module path.
Trying to use import module with the unmodified artifact on the module path, using it as an automatic module exporting all its packages, yields:
javac --module-path junit-platform-console-standalone-1.13.0.jar --add-modules ALL-MODULE-PATH Test.java
Test.java:3: error: reference to Test is ambiguous
@Test void test() {}
^
both interface junit.framework.Test in junit.framework and class org.junit.Test in org.junit match
Test.java:3: error: compact source file does not have main method in the form of void main() or void main(String[] args)
@Test void test() {}
^
2 errors
An explicit module descriptor though, as you suggested, might help. 🤔
I took your compact source file and moved most of the command-line into a Test.java file:
import org.junit.jupiter.api.*;
void main() {
var junit = ToolProvider.findFirst("junit").orElseThrow();
var error = junit.run(System.out, System.err, "execute",
"--select-class", getClass().getName(), // "Test"
"--select-class", A.class.getName(), // "Test$A"
"--select-class", B.class.getName() // "Test$B"
);
System.exit(error);
}
@Test
void t() {}
@RepeatedTest(3)
void r() {}
static class A {
@Test void a() {}
}
@Nested class B {
@Test void b() {}
}
Together with a standalone JUnit Platform artifact in the current working directory, one just calls java --class-path * Test.java to get:
Thanks for using JUnit! Support its development at https://junit.org/sponsoring
╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ ├─ Test ✔
│ │ ├─ B ✔
│ │ │ └─ b() ✔
│ │ ├─ r() ✔
│ │ │ ├─ repetition 1 of 3 ✔
│ │ │ ├─ repetition 2 of 3 ✔
│ │ │ └─ repetition 3 of 3 ✔
│ │ └─ t() ✔
│ └─ Test$A ✔
│ └─ a() ✔
└─ JUnit Vintage ✔
Test run finished after 75 ms
[ 7 containers found ]
[ 0 containers skipped ]
[ 7 containers started ]
[ 0 containers aborted ]
[ 7 containers successful ]
[ 0 containers failed ]
[ 6 tests found ]
[ 0 tests skipped ]
[ 6 tests started ]
[ 0 tests aborted ]
[ 6 tests successful ]
[ 0 tests failed ]
Yes, on the good ol' class path. Yes, no import module - but also yes, using the java launcher in source mode. 🤓
Back to "import module ...": if you replace the standalone JAR with the equivalent set of explicit modules, the import module org.junit.jupiter; statement does work as expected. With Test.java reading:
import module org.junit.jupiter;
@Test
void test() {}
void main() {
var junit = ToolProvider.findFirst("junit").orElseThrow();
var error = junit.run(System.out, System.err, "execute", "--select-class", getClass().getName());
System.exit(error);
}
and all modular JAR files required for Jupiter in a lib directory, running:
java --module-path lib --add-modules ALL-MODULE-PATH Test.java
yields:
╷
└─ JUnit Jupiter ✔
└─ Test ✔
└─ test()
How to get all "Jupiter modules" into a lib directoy without a build tool? One option is to use jresolve: https://github.com/bowbahdoe/jresolve-cli
java -jar jresolve.jar \
--output-directory=lib \
--purge-output-directory \
--use-module-names \
pkg:maven/org.junit.jupiter/[email protected]
pkg:maven/org.junit.platform/[email protected]
(About your last but one comment) Looks very nice and straightforward; I might tweak your main, turning it into a helper function which will allow the user to just go void main() { testThisClass(); } or something close enough.
The underlying reason for this issue is that I intend to make use of the new language features to teach beginners about basic testing while balancing keeping everything as low-level as possible with as simple as possible. Thus, no frameworks.
One possible simplification is if I can tell the students to remember simply writing a single import module directive vs telling them, "you'll need to use the following:"
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.*;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
Your most recent comment is indeed a way, only it seems too finicky for beginners.
(About your last but one comment) Looks very nice and straightforward; I might tweak your main, turning it into a helper function which will allow the user to just go void main() { testThisClass(); } or something close enough.
With the multi-file source-code launcher you may extract such a feature into a sibling source file, for example JUnitSupport.java:
class JUnitSupport {
static void run(Class<?> test) {
var junit = java.util.spi.ToolProvider.findFirst("junit").orElseThrow();
var error = junit.run(System.out, System.err, "execute", "--select-class", test.getName());
System.exit(error);
}
}
And then just: void main() { JUnitSupport.run(getClass()); }
Another approach would be a convenient execution routine in the Launcher API of the Platform itself. 🤔 @junit-team?
The underlying reason for this issue is that I intend to make use of the new language features to teach beginners about basic testing while balancing keeping everything as low-level as possible with as simple as possible. Thus, no frameworks.
Thus, no JUnit framework. Use asserts, Luke! (-;
Your most recent comment is indeed a way, only it seems too finicky for beginners.
Yeah. We're working on it. The Java On-Ramp is not done yet.
One pain-point is the --add-modules ALL-MODULE-PATH sourcery ... which could be automatically added in source launcher mode:
- https://bugs.openjdk.org/browse/JDK-8334761
- https://github.com/openjdk/jdk/pull/19842/files
Yes, that's my idea as well, but instead of using getClass() which would require an unsuitably deep explanation, I (or your team if you wish to implement it, it's a warmup exercise) can just use the stack trace to fish out the class info without the caller ever knowing. The devil's in the details, as always: my version would be a very restrictive, trivial one-shot, but if you add a thing like this, you're stuck with it for a long time (in a galaxy very, very near)...
JUnit as a standalone, for this purpose, functions as a library on my end, plus a main that requires a slightly fancy invocation. I hide al(most al)l nasty details from the end users. 😀🪄
You could even adopt a basic, recommended structure for the upcoming JUnit 6 (provided that the user has Java 25+ or less conveniently, Java 23/24 with preview features on) which would look like this:
import module org.junit.jupiter;
import static org.junit.jupiter.api.Assertions.*;
void main() { runAllTests(); }
// tests...
If only that pesky static import could go away somehow...
Yes, that's my idea as well, but instead of using getClass() which would require an unsuitably deep explanation, I (or your team if you wish to implement it, it's a warmup exercise) can just use the stack trace to fish out the class info without the caller ever knowing. The devil's in the details, as always: my version would be a very restrictive, trivial one-shot, but if you add a thing like this, you're stuck with it for a long time (in a galaxy very, very near)...
Yeah, sounds not too stable. I'd add an overload to JUnitSupport like this:
static void run(Object that) {
run(that instanceof Class test ? test : that.getClass());
}
Which, in case of an instance main method (in a compact source file), enables this neat call:
void main() { JUnitSupport.run(this); }
If only that pesky static import could go away somehow...
In former times, we used to extend a (n abstract) test class to inherit some methods from. We're not going back there, though. See this note attached to Assertions API documentation:
Extensibility Although it is technically possible to extend this class, extension is strongly discouraged. The JUnit Team highly recommends that the methods defined in this class be used via
static imports.
One pain-point is the --add-modules ALL-MODULE-PATH sourcery ... which could be automatically added in source launcher mode:
https://bugs.openjdk.org/browse/JDK-8334761 https://github.com/openjdk/jdk/pull/19842/files
Hey, you quoted me!
We'll explore the full-modular variant in
- #4982
After that, I'll also take a look at how to make the standalone JAR workable in compact source files, again. Might not work out as smooth as with true modules.
import static org.junit.jupiter.api.Assertions.*;If only that pesky static import could go away somehow...
IO.println(...)JUnit.run(...)Assertions.assert...(...)
I see a pattern here. Let's stick to it. Leaves room to introduce another library here:
Assert.that(...)...;