When testing a JPMS modular project, how do you pass module-path / module-system arguments (like --add-opens or --add-modules) to the test JVM in Gradle?
answer
- module flags are just jvmArgs
- --add-opens / --add-exports / --add-reads / --add-modules / --patch-module
- each space-separated token = separate list element
- module-info.java -> Gradle builds module path automatically
- JDK upgrade -> InaccessibleObjectException -> add-opens
basics
~10 sAdd the module flags to the test task's jvmArgs, e.g. jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") or --add-modules. For real JPMS modular runs Gradle can also build a module path automatically when a module-info.java is present.
solid answer
~40 sModule-system flags are ordinary JVM arguments, so you pass them to the forked test worker via the `test` task's `jvmArgs`: `jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")`, `jvmArgs("--add-modules", "jdk.incubator.vector")`, `--add-reads`, `--patch-module`, etc. Note `--add-opens` takes a single `module/package=target` token plus the value as separate list elements. For a genuinely modular test run, Gradle's java-library/JPMS support will, when a `module-info.java` exists, place compiled outputs on the **module path** instead of the classpath automatically; you then only add the extra opens/reads needed by reflective test frameworks. A frequent real case: test/mocking/serialization libraries reflect into JDK internals on newer JDKs, requiring explicit `--add-opens` because strong encapsulation now denies deep reflection by default.
code
kotlin · 9 linestasks.test {
// Grant reflective access denied by strong encapsulation on newer JDKs
jvmArgs(
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.base/java.util=ALL-UNNAMED",
)
// Resolve an incubator module for the test run
jvmArgs("--add-modules", "jdk.incubator.vector")
}go deeper
Know module flags can be added via jvmArgs even if you don't memorize each one.
Pass --add-opens/--add-modules correctly as separate tokens; recognize the InaccessibleObjectException-on-upgrade pattern.
Understand module-path inference from module-info.java, --patch-module for white-box tests, and granting minimal targeted access.
Set a policy for module access in tests (minimal opens, documented rationale) and standardize JDK-upgrade migration flags across the org.
## Background: the Java module system at test time Since Java 9, the JVM can run code on a **module path** with **strong encapsulation**: a module only exposes packages it `exports`, and deep reflection into another module's internals is denied unless explicitly `opens`-ed. On modern JDKs this is enforced even for the unnamed module, so libraries that reflect into JDK internals (some mocking, serialization, and DI frameworks) fail at runtime unless you grant access. ## These are just JVM args All module flags are normal command-line arguments, so they go through the test task's `jvmArgs`: - `--add-opens module/package=target` — open a package for deep reflection. - `--add-exports module/package=target` — make a non-exported package readable. - `--add-reads module=other` — add a readability edge. - `--add-modules name` — resolve modules not otherwise required (e.g. incubator modules). - `--patch-module name=path` — inject test classes into a module (common when white-box testing a module's internals). Watch the tokenization: each *space-separated* piece is a **separate** list element. `--add-opens` and its `java.base/java.lang=ALL-UNNAMED` value are two elements: ```kotlin tasks.test { jvmArgs( "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-modules", "jdk.incubator.vector", ) } ``` ## Module path vs classpath for tests When your `src/main/java` has a `module-info.java`, Gradle's Java plugins build a **module path** for that code automatically. Test code typically still runs on the classpath as the *unnamed module* (so frameworks can reflect freely), which is why most projects only need a few `--add-opens`. If you want white-box tests *inside* the module, you use `--patch-module` to splice test classes into your module, plus the reads/opens that implies. Some setups also set `modularity.inferModulePath` to control this inference. ## Why this comes up The most common interview-relevant scenario is upgrading the test JDK: a previously-fine suite starts throwing `InaccessibleObjectException` because newer JDKs deny deep reflection by default. The fix is a targeted `--add-opens` on the test task's `jvmArgs`, not disabling encapsulation globally. ## Verifying Because workers are separate processes, run with `--info` to see the exact launched command line and confirm your module flags were applied to the right worker.
- After upgrading the test JDK, mocking tests fail with InaccessibleObjectException. What changed and how do you fix it?Newer JDKs enforce strong encapsulation, denying deep reflection into JDK internals by default. Add targeted `--add-opens module/package=ALL-UNNAMED` flags to the test task's jvmArgs rather than disabling encapsulation wholesale.
- Why must --add-opens and its argument be separate elements in jvmArgs(...)?jvmArgs is a list of individual command-line tokens; the JVM expects `--add-opens` followed by a separate `module/package=target` token, so combining them into one string would be parsed incorrectly.
- How would you white-box test the internals of your own JPMS module?Use --patch-module to splice the test classes into your module so they can access package-private/internal members, plus any required --add-reads/--add-opens.
saying these in an interview costs you the question
- Treating module flags as something special instead of plain jvmArgs.
- Concatenating --add-opens and its value into one string element.
- Disabling encapsulation broadly instead of granting targeted access.