Hello,

During the spring boot upgrade 3.4.0 i am getting below error which is actually working fine with 3.3.6.

org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 3.
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:64)
    at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base@17.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base@17.0.11/java.lang.reflect.Method.invoke(Method.java:568)
    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:94)
    at jdk.proxy2/jdk.proxy2.$Proxy5.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
    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.NoClassDefFoundError: org/junit/platform/launcher/TestExecutionListener
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:467)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1217)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1228)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1273)
    at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1309)
    at java.base/java.util.ServiceLoader$3.hasNext(ServiceLoader.java:1393)
    at java.base/java.util.Iterator.forEachRemaining(Iterator.java:132)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1845)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
    at org.junit.platform.launcher.core.LauncherFactory.registerTestExecutionListeners(LauncherFactory.java:179)
    at org.junit.platform.launcher.core.LauncherFactory.createDefaultLauncher(LauncherFactory.java:137)
    at org.junit.platform.launcher.core.LauncherFactory.create(LauncherFactory.java:125)
    at org.junit.platform.launcher.core.LauncherFactory.create(LauncherFactory.java:109)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:97)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
    ... 18 more
Caused by: java.lang.ClassNotFoundException: org.junit.platform.launcher.TestExecutionListener
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    ... 50 more

Could someone pls help me to sort out the issue.

Comment From: bclozel

duplicates #43339 please ask your question on StackOverflow as asked previously.

Comment From: ledigiacomo

I am running into the same issues as the original post here when upgrading from Spring Boot 3.3.+ to Spring Boot 3.4.0. We have approx. 100 projects all building with Gradle 7.6.1, and see the above error in roughly 1/3 of the projects where tests pass with Spring Boot 3.3.5. The other 2/3 see to run without issue. We're working on figuring out what distinguishes the failing projects from the working projects and will put together a small sample app if we can.

As recommended in the StackOverflow post, we can add the dependency org.junit.platform:junit-platform-launcher to our failing projects and get tests to pass. We have also noticed that Spring Initializer has added the dependency to all Gradle based projects by default: https://github.com/spring-io/initializr/issues/1476. I've left a comment there, but Ill ask the question here as well to try to get some traction on it: is the expectation that all Gradle based Spring Boot projects that use JUnit should include this dependency? If not all projects, what do we need to look for to know whether we need to add the dependency or not?

If the answers to these questions is that the dependency is now expected, I think at the least that it would benefit the community if a note was added to the Migration Guide about the expectation and pushed under this ticket

Comment From: wilkinsona

is the expectation that all Gradle based Spring Boot projects that use JUnit should include this dependency?

No.

If not all projects, what do we need to look for to know whether we need to add the dependency or not?

The dependency is needed if you're using Gradle 8.3 or later, you're not using test suites, and you do not want to see a warning about the "automatic loading of test framework implementation dependencies" being deprecated. See https://docs.gradle.org/8.3/userguide/upgrading_version_8.html#test_framework_implementation_dependencies for further details.

Comment From: stephsmithnc

I can recreate this with Gradle 7.6.4. I used Spring Initializr to create the demo with Spring Boot 3.4.0. I then updated the build.gradle. to remove the junit-platform-launcher dependency and added a dependency on spring-boot-actuator-autoconfigure. I then updated the project to use Gradle 7.6.4 instead of Gradle 8.11.1. This reproduces the ClassNotFoundException. The build.gradle now defined with:

implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-actuator-autoconfigure'

Should we be adding the junit-platform-launcher explicitly in this scenario?

Comment From: wilkinsona

I’m not sure that I understand what you’ve described. Please share the project that fails with a ClassNotFoundException and we can take a look.

Comment From: stephsmithnc

You can find the example at spring-boot-issue-43340.

These were the steps I took to create my project that recreates the ClassNotFoundException

  • using spring initializr to create the demo
  • updated the code to use Gradle 7.6.4 as that is what we are using
  • added the spring-boot-actuator-autoconfigure because we require that in our environment where we are seeing the ClassNotFoundException

If the project is updated to use Gradle 8.11.1, I do not see the exception. We are using Gradle 7.6.4 and have these dependencies. Would like to know if the workaround is to explicitly add the junit-platform-launcher dependency? Thanks!

Comment From: wilkinsona

Thanks very much, @stephsmithnc. That sample has helped me to identify the root cause of the problem.

The problem's caused by OpenTelemetryEventPublisherBeansTestExecutionListener and the class loader arrangement when Gradle is bootstrapping JUnit. org.junit.platform.launcher.core.LauncherFactory is loaded from within the Gradle distribution from where it also loads org.junit.platform.launcher.TestExecutionListener. The ServiceLoader is then asked to load any TestExecutionListener implementations, but using a different class loader to that which loaded LauncherFactory. This different class loader (it's the JDK's AppClassLoader) cannot see org.junit.platform.launcher.TestExecutionListener so the NoClassDefFoundError occurs.

This sort of problem is exactly what Gradle 8 seeks to avoid by deprecating support for automatically loading test implementation classes. We'll discuss this as a team, but a reasonable workaround for now is to declare a dependency on org.junit.platform:junit-platform-launcher if you're using org.springframework.boot:spring-boot-actuator-autoconfigure in your app.

Comment From: philwebb

I've got a branch that "fixes" this by migrating from a TestExecutionListener to a TestEngine. It feels quite hacky, but you could argue that the TestExecutionListener approach is also a hack.

Flagging to see if the team think we should apply the "fix" or document the suggested workaround.

Comment From: wilkinsona

With the TestExecutionListener approach, the lifecycle was roughly right. I don't feel the same with the TestEngine approach and think that's more of a hack. OpenTelemetry's reliance on statics is a broader problem than just Spring Boot and I don't think we should add hacks that try to work around it.

I wonder if we could hook something in using SettableContextStorageProvider. It appears to be specific to testing but doesn't rely on any particular testing framework so it could be a good fit here.

If SettableContextStorageProvider doesn't work out, I think we should revert the original change. We could then perhaps document the suggested workaround and encourage those affected to request some improvements in OpenTelemetry so that the problem can be addressed at source.

Comment From: wilkinsona

I wonder if we could hook something in using SettableContextStorageProvider.

This doesn't work as it's ContextStorageWrappers that is marked as initialized and that prevents the addition of any other wrappers. It's statically scoped with no means to clear or reinitialise it even when using a custom context storage provider.

I think we should revert our workaround in favor of an enhancement request for OpenTelemetry to add some testing support to ContextStorageWrappers alongside the existing related support that's provided by SettableContextStorageProvider.

Comment From: philwebb

I think I'd prefer to keep the existing code and document the fix for Gradle users. The existing code works for IDEs, modern Gradle and Maven and fixes an issue that's pretty hard to discover. At least the Gradle failure is easy to Google.

Comment From: wilkinsona

I've opened https://github.com/spring-projects/spring-boot-release-verification/issues/3 so that we can catch this sort of problem during release verification.