[Test][ClassLoader] Quarkus 3.22 throws when tests run with junit-platform.properties resource file
Describe the bug
Might be related to https://quarkus.io/blog/test-classloading-rewrite/
If you create a junit-platform.properties file in src/test/resources and run ./gradlew test it throws
java.lang.RuntimeException: Internal error. The test class class com.beachape.GreetingResourceTest should have been loaded with a QuarkusClassLoader, but instead it was loaded with jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418. This is caused by the FacadeClassLoader not correctly identifying this class as a QuarkusTest.
Prior to 3.22, this wouldn't throw.
Expected behavior
It should run.
Actual behavior
It throws
java.lang.RuntimeException: Internal error. The test class class com.beachape.GreetingResourceTest should have been loaded with a QuarkusClassLoader, but instead it was loaded with jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418. This is caused by the FacadeClassLoader not correctly identifying this class as a QuarkusTest.
at io.quarkus.test.junit.QuarkusTestExtension.getClassLoaderFromTestClass(QuarkusTestExtension.java:337)
at io.quarkus.test.junit.QuarkusTestExtension.ensureStarted(QuarkusTestExtension.java:631)
at io.quarkus.test.junit.QuarkusTestExtension.beforeAll(QuarkusTestExtension.java:712)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
How to Reproduce?
https://github.com/lloydmeta/quarkus-3.22-test-bug-repo
Output of uname -a or ver
24.4.0 Darwin Kernel Version 24.4.0: Fri Apr 11 18:28:23 PDT 2025; root:xnu-11417.101.15~117/RELEASE_X86_64 x86_64
Output of java -version
java 23.0.2 2025-01-21
Quarkus version or git rev
3.22.1
Build tool (ie. output of mvnw --version or gradlew --version)
Gradle 8.13
Additional information
❯ ./gradlew test --no-build-cache
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.21.4/1754ab3d86388a2957df43f208fbc8f9417c47eb/quarkus-junit5-config-3.21.4.jar!/junit-platform.properties
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.21.4/1754ab3d86388a2957df43f208fbc8f9417c47eb/quarkus-junit5-config-3.21.4.jar!/junit-platform.properties
[Incubating] Problems report is available at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/problems/problems-report.html
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 31s
11 actionable tasks: 11 executed
❯ ./gradlew test --no-build-cache
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.22.1/d44e0e34a555e8291c2d8af1cef830283d387b85/quarkus-junit5-config-3.22.1.jar!/junit-platform.properties
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.22.1/d44e0e34a555e8291c2d8af1cef830283d387b85/quarkus-junit5-config-3.22.1.jar!/junit-platform.properties
> Task :test FAILED
GreetingResourceTest > initializationError FAILED
java.lang.RuntimeException at QuarkusTestExtension.java:337
1 test completed, 1 failed
[Incubating] Problems report is available at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 5s
11 actionable tasks: 9 executed, 2 up-to-date
Workarounds
- Going back to 3.21.4 works
- Removing the properties file works
I wonder if my own issue is related? If you have a package-private constructor or method in one (gradle) subproject (call it "A") and try to use it in another subproject (call it "tests") via extending the class (yes, split-package is discouraged, and this is probably why, but I digress), the class in "tests" is loaded in a different classloader than the parent class in "A", causing an IllegalAccessError:
project_common/src/main/java/com/example/ExampleInterface.java:
public interface ExampleInterface {
List<String> performSomeAlgorithm(POJO object, int count);
}
project_A/src/main/java/com/example/ProductionImplementationClass.java:
public class ProductionImplementationClass implements ExampleInterface {
public ProductionImplementationClass(String param) {
// impl elided
}
@VisibleForTesting
ProductionImplementationClass() {
// impl elided
}
@VisibleForTesting
List<String> performSomeAlgorithm(POJO object, int count) {
// impl elided
}
}
tests/src/main/java/com/example/TestProductionImplementationClass.java:
public class TestProductionImplementationClass extends ProductionImplementationClass {
List<String> testPerformSomeAlgorithm(POJO object, int count) {
return performSomeAlgorithm(object, count);
}
}
This compiles perfectly fine, but when it's executed we find that ProductionImplementationClass is loaded in a different classloader than TestProductionImplementationClass so cannot invoke the constructor (either implicitly or explicitly) nor the method under test. Note that the test class is in the main source set: this is intentional.
For context: in our service, project_common is a subproject that contains code common to all applications contained in the root project, project_A is one self-contained application, and tests is our test framework and tests. project_A depends on project_common via a gradle implementation dependency, and tests depends on both also via a gradle implementation dependency declaration.
This exact arrangement functions as-is in 3.21.4 but breaks on upgrade to 3.22.1. I'm not opposed to changing the visibility of the methods/ctors in question, but I wonder what exact change may have led to this and whether it could be a bug or intended behaviour. And, yes, split-packages are discouraged, and I should probably make the change if only for that reason alone, but again... same question: bug or intentional.
@kevinross could you maybe create a separate issue with a reproducer? I'm not sure both are related and it's probably better to track them separately.
/cc @geoand (testing)
/cc @holly-cummins
Ah yes, this is definitely related to my changes. There may be a workaround, I'll have a look.
Should I create a new ticket then...? I commented on this one due to the clear connection to classloaders in the stacktrace - but I'm happy to create a new one and attach a proper reproducer.
Yes please, @kevinross. The root cause is most likely the same (https://github.com/quarkusio/quarkus/pull/34681), but I suspect the fix for your issue will be very different. The changes in 3.22 are intended to avoid the sort of "tests in wrong classloader" problem you're seeing but obviously didn't have the intended effect for your scenario.
@holly-cummins here you go!
This is still an issue in 3.22.3 but now the stack trace is different:
org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 21.
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:65)
at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at [email protected]/java.lang.reflect.Method.invoke(Method.java:580)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92)
at jdk.proxy1/jdk.proxy1.$Proxy4.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:200)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.NullPointerException: Cannot invoke "io.quarkus.test.junit.classloading.FacadeClassLoader.isServiceLoaderMechanism()" because "io.quarkus.test.junit.launcher.CustomLauncherInterceptor.facadeLoader" is null
at io.quarkus.test.junit.launcher.CustomLauncherInterceptor.launcherDiscoveryFinished(CustomLauncherInterceptor.java:101)
at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.lambda$launcherDiscoveryFinished$1(CompositeLauncherDiscoveryListener.java:45)
at org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder(CollectionUtils.java:243)
at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.launcherDiscoveryFinished(CompositeLauncherDiscoveryListener.java:45)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:110)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:78)
at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:99)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39)
at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25)
at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:124)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:94)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
... 16 more
This issue is not just related to gralde, maven has the same issue.
Reproducer:
quarkus create app org.acme:getting-started
cd getting-started
mkdir -p src/test/resources/
touch src/test/resources/junit-platform.properties
mvn clean verify
Running Apache Maven 3.9.9 with jdk 21
Thanks for the update, @rodcheater! And thanks for letting us know about maven, @jagodevreede. I've updated the blog to reflect the broader scope of the issue.
This is still an issue in 3.22.3 but now the stack trace is different:
org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 21. at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:65) at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at [email protected]/java.lang.reflect.Method.invoke(Method.java:580) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92) at jdk.proxy1/jdk.proxy1.$Proxy4.stop(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:200) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71) at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) Caused by: java.lang.NullPointerException: Cannot invoke "io.quarkus.test.junit.classloading.FacadeClassLoader.isServiceLoaderMechanism()" because "io.quarkus.test.junit.launcher.CustomLauncherInterceptor.facadeLoader" is null at io.quarkus.test.junit.launcher.CustomLauncherInterceptor.launcherDiscoveryFinished(CustomLauncherInterceptor.java:101) at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.lambda$launcherDiscoveryFinished$1(CompositeLauncherDiscoveryListener.java:45) at org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder(CollectionUtils.java:243) at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.launcherDiscoveryFinished(CompositeLauncherDiscoveryListener.java:45) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:110) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:78) at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:99) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85) at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47) at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39) at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25) at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38) at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:124) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:99) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:94) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63) ... 16 more
We are experiencing this same issue when trying to upgrade to 3.22.3. I gave 3.23.0.CR1 a shot as well but it's still there. 3.20.1 works fine.
As a workaround for this, putting the following into your project's junit-platform.properties file should get things working:
junit.jupiter.extensions.autodetection.enabled=true
junit.jupiter.testclass.order.default=io.quarkus.test.config.QuarkusClassOrderer
junit.platform.launcher.interceptors.enabled=true
@holly-cummins that seems to work. thanks!
I've got a PR now which fixes this. But in general, having a junit-platform.properties can still cause problems in some cases. @lspahija, if you're using the properties to register a class orderer, it's better to list that orderer in the Quarkus application properties (see https://quarkus.io/guides/getting-started-testing#testing_different_profiles).