When developers first adopt the Java Platform Module System (JPMS), they often face a frustrating issue: tests fail to run after adding module-info.java
. Common errors include “package not visible” or “module not found.” This happens because test frameworks like JUnit and TestNG rely heavily on reflection, which is restricted by JPMS.
Testing strategies for modular applications are critical in real-world projects. Large enterprise applications, microservices, and modularized legacy systems all depend on reliable testing pipelines to ensure quality and maintainability. Without proper test configurations, developers waste time debugging tooling rather than validating business logic.
Think of JPMS like an office building with locked departments. Tests are inspectors—they need temporary keys (opens
) to verify everything inside. Without those keys, inspections fail. Designing modular applications requires testing strategies that provide inspectors access without compromising overall security.
Challenges in Testing Modular Applications
-
Reflection Restrictions
JUnit and TestNG use reflection to discover and invoke tests. If packages aren’topens
to them, tests fail. -
Module Boundaries
Tests may require access to non-exported implementation details. -
Build Tool Integration
Maven and Gradle need explicit configuration to pass the module path to test runners.
Strategies for JUnit
Example module-info.java
module com.example.app {
exports com.example.app.api;
opens com.example.app.internal to org.junit.platform.commons;
}
Here, the internal
package is opened only to JUnit for testing purposes.
Maven Surefire Plugin for JUnit
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<argLine>
--add-opens com.example.app/com.example.app.internal=org.junit.platform.commons
</argLine>
</configuration>
</plugin>
Strategies for TestNG
Example module-info.java
module com.example.app {
exports com.example.app.api;
opens com.example.app.internal to org.testng;
}
Gradle Configuration for TestNG
test {
useTestNG()
jvmArgs += [
'--add-opens', 'com.example.app/com.example.app.internal=org.testng'
]
}
Best Practices & Pitfalls
✅ Best Practices
- Use
opens
for test frameworks instead ofopen
(granular access) - Keep tests in separate modules (e.g.,
com.example.app.test
) - Use build tool configs (
--add-opens
) for fine-grained testing access - Automate testing in CI/CD pipelines with modular awareness
❌ Pitfalls
- Exporting internal packages just for testing (breaks encapsulation)
- Using
open
on entire modules (unnecessary exposure) - Ignoring reflective access issues (leads to brittle tests)
- Mixing test and production code in the same module
Example: Dedicated Test Module
Production module-info.java
module com.example.library {
exports com.example.library.api;
}
Test module-info.java
open module com.example.library.test {
requires com.example.library;
requires org.junit.jupiter.api;
}
- Production module exports only APIs
- Test module is
open
to allow reflection for test frameworks
What's New in Java Versions?
- Java 5–8 → N/A (no modules)
- Java 9 → JPMS introduced, created challenges for reflective test frameworks
- Java 11 → Tooling improved for JUnit/TestNG with modules
- Java 17 → Stable ecosystem for modular testing
- Java 21 → No significant updates across Java versions for this feature
Real-World Analogy
Testing modular apps is like health inspections in secure buildings. Inspectors (tests) don’t need permanent keys to every office; they get temporary passes (opens
) to check specific areas. This balance ensures thorough inspections without sacrificing security.
Summary & Key Takeaways
- JPMS restricts reflection, making modular test configuration essential
- Use
opens
or--add-opens
for JUnit/TestNG access - Keep tests in dedicated modules for better separation
- Avoid overexposing internals just for testing convenience
- Modern tooling in Maven and Gradle supports modular testing with minor setup
FAQ: Testing Modular Applications
1. What is the difference between the classpath and module path for tests?
Classpath loads everything blindly, module path enforces visibility rules.
2. Why do I get “package not visible” errors when running tests?
Because the package isn’t exported or opened to the test framework.
3. What’s the purpose of requires transitive
in testing?
It allows test modules to inherit dependencies automatically.
4. How do open
and opens
differ for testing?open
opens the entire module, while opens
restricts reflection to specific packages.
5. Do automatic modules affect testing?
Yes, they may cause inconsistent visibility issues. Use explicit modules instead.
6. How does JPMS improve security while testing?
It ensures only explicitly opened packages are accessible, limiting unintended exposure.
7. Should I use jlink or jmod in testing pipelines?
Use jlink
for runtime images, jmod
for packaging dependencies. Testing usually runs before packaging.
8. Can I modularize incrementally and still test?
Yes, use automatic modules temporarily but transition to explicit modules for stability.
9. How do I handle third-party non-modular libraries in tests?
Place them on the classpath or use automatic modules cautiously.
10. Do frameworks like Spring and Hibernate work with modular tests?
Yes, but they often need opens
for reflective access to entities and beans.