Skip to content

NoClassDefFoundError when using JUnit to test a Gradle 7.6.x app that depends on spring-boot-actuator-autoconfigure but not on org.junit.platform:junit-platform-launcher #43340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
saravanakumarmuthukumar1 opened this issue Dec 2, 2024 · 13 comments
Assignees
Labels
type: regression A regression from a previous release
Milestone

Comments

@saravanakumarmuthukumar1
Copy link

saravanakumarmuthukumar1 commented Dec 2, 2024

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 [email protected]/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at [email protected]/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at [email protected]/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at [email protected]/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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 2, 2024
@bclozel
Copy link
Member

bclozel commented Dec 2, 2024

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

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale Dec 2, 2024
@bclozel bclozel added status: invalid An issue that we don't feel is valid for: stackoverflow A question that's better suited to stackoverflow.com and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 2, 2024
@ledigiacomo
Copy link

ledigiacomo commented Jan 3, 2025

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: spring-io/initializr#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

@wilkinsona
Copy link
Member

wilkinsona commented Jan 3, 2025

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.

@stephsmithnc
Copy link

stephsmithnc commented Jan 3, 2025

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?

@wilkinsona
Copy link
Member

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.

@stephsmithnc
Copy link

stephsmithnc commented Jan 6, 2025

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!

@wilkinsona
Copy link
Member

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.

@wilkinsona wilkinsona changed the title java.lang.NoClassDefFoundError: org/junit/platform/launcher/TestExecutionListener & java.lang.ClassNotFoundException: org.junit.platform.launcher.TestExecutionListener NoClassDefFoundError when using JUnit to test a Gradle app that depends on spring-boot-actuator-autoconfigure but not on org.junit.platform:junit-platform-launcher Jan 6, 2025
@wilkinsona wilkinsona added type: regression A regression from a previous release and removed status: invalid An issue that we don't feel is valid for: stackoverflow A question that's better suited to stackoverflow.com labels Jan 6, 2025
@wilkinsona wilkinsona added this to the 3.4.x milestone Jan 6, 2025
@wilkinsona wilkinsona added the for: team-meeting An issue we'd like to discuss as a team to make progress label Jan 6, 2025
@wilkinsona wilkinsona reopened this Jan 6, 2025
@philwebb
Copy link
Member

philwebb commented Jan 7, 2025

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.

@philwebb philwebb added for: team-attention An issue we'd like other members of the team to review and removed for: team-meeting An issue we'd like to discuss as a team to make progress labels Jan 7, 2025
@wilkinsona
Copy link
Member

wilkinsona commented Jan 7, 2025

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.

@wilkinsona
Copy link
Member

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.

@philwebb
Copy link
Member

philwebb commented Jan 7, 2025

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.

@wilkinsona wilkinsona added for: team-meeting An issue we'd like to discuss as a team to make progress and removed for: team-attention An issue we'd like other members of the team to review labels Jan 8, 2025
@philwebb philwebb removed the for: team-meeting An issue we'd like to discuss as a team to make progress label Jan 8, 2025
@wilkinsona
Copy link
Member

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.

@wilkinsona wilkinsona changed the title NoClassDefFoundError when using JUnit to test a Gradle app that depends on spring-boot-actuator-autoconfigure but not on org.junit.platform:junit-platform-launcher NoClassDefFoundError when using JUnit to test a Gradle 7.6.x app that depends on spring-boot-actuator-autoconfigure but not on org.junit.platform:junit-platform-launcher Jan 17, 2025
@wilkinsona wilkinsona modified the milestones: 3.4.x, 3.4.2 Jan 17, 2025
@wilkinsona wilkinsona marked this as a duplicate of #43889 Jan 21, 2025
@alexisgayte
Copy link

Many thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: regression A regression from a previous release
Projects
None yet
Development

No branches or pull requests

8 participants