testcontainers-java icon indicating copy to clipboard operation
testcontainers-java copied to clipboard

GenericContainer run from Jupiter tests shouldn't require JUnit 4.x library on runtime classpath

Open bmuschko opened this issue 7 years ago • 71 comments

I tried out the JUnit 5 support using the following build script. I'd expect that TestContainers doesn't require the JUnit 4.x library. As you can see in the build script below, the legacy dependency has been excluded.

apply plugin: 'java-library'

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
version = '1.0'

repositories {
    jcenter()
}

dependencies {
    def junitJupiterVersion = '5.3.1'
    testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion"
    testImplementation "org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion"
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion"
    testImplementation 'org.testcontainers:junit-jupiter:1.10.1'
}

// IMHO shouldn't be required
configurations.all {
   exclude group: 'junit', module: 'junit'
}

tasks.withType(Test) {
    useJUnitPlatform()
}

In my test case, I am creating a GenericContainer.

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public class TestContainerJunit5Test {
    @Container
    private GenericContainer appContainer = new GenericContainer();

    @Test
    void tryItOut() {
        // do something
    }
}

At runtime I get the following exception. My guess is that GenericContainer still uses JUnit 4.x classes.

java.lang.NoClassDefFoundError: org/junit/rules/TestRule
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.getDeclaredFields0(Native Method)
	at java.lang.Class.privateGetDeclaredFields(Class.java:2583)
	at java.lang.Class.getDeclaredFields(Class.java:1916)
	at org.junit.platform.commons.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:1106)
	at org.junit.platform.commons.util.ReflectionUtils.findAllFieldsInHierarchy(ReflectionUtils.java:886)
	at org.junit.platform.commons.util.ReflectionUtils.findFields(ReflectionUtils.java:874)
	at org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields(AnnotationUtils.java:320)
	at org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields(AnnotationUtils.java:297)
	at org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields(ExtensionUtils.java:92)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.prepare(ClassTestDescriptor.java:154)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.prepare(ClassTestDescriptor.java:74)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$0(NodeTestTask.java:80)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:80)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:66)
	at java.util.ArrayList.forEach(ArrayList.java:1257)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:110)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:71)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:92)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$100(JUnitPlatformTestClassProcessor.java:77)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:73)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:131)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:155)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:137)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassNotFoundException: org.junit.rules.TestRule
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 80 more

bmuschko avatar Nov 09 '18 03:11 bmuschko

Hi @bmuschko,

We very much agree, and we do plan to remove the dependency, but it will be a breaking change and we will do it in Testcontainers 2.0. We were planning to start 2.0 earlier but discovered a few low hanging fruits (like the JUnit 5 extension, or OkHttp transport), this is why it got delayed. But now probably is a good time to finally focus on 2.0 :)

/cc @rnorth @kiview

bsideup avatar Nov 09 '18 07:11 bsideup

Also see #87 with some more (historic) discussions regarding this topic.

kiview avatar Nov 09 '18 08:11 kiview

Great, thanks for confirming. Looking forward to 2.0.

bmuschko avatar Nov 09 '18 14:11 bmuschko

I ran into this issue as well and it's currently a blocker for me. Is there any estimated date when 2.0 will be released? Where can I find roadmap?

The product looks very promising btw :) looking forward to 2.0 as well so I can finally start using it

gosiapolitowska avatar Jan 10 '19 21:01 gosiapolitowska

Sorry, we haven't updated the roadmap for quite some time :slightly_smiling_face: Why exactly is it a blocker for you?

kiview avatar Jan 10 '19 21:01 kiview

wow, that was a quick response :) we switched to Junit5 and are explicitly excluding Junit4 from all dependencies to make sure people don't use it accidentally or out of habit

gosiapolitowska avatar Jan 10 '19 21:01 gosiapolitowska

I see, makes sense for some projects. We would love to remove this dependency rather sooner than later, however since it will be a breaking change, we haven't decided on when to actually do it yet.

kiview avatar Jan 10 '19 21:01 kiview

Any updates on this one?

evonier avatar Aug 05 '19 13:08 evonier

@evonier no updates since the last acknowledgement of the problem and the promise to target it for 2.x version.

bsideup avatar Aug 05 '19 13:08 bsideup

Updating testcontainers from version 1.11.4 to version 1.12.0 fixed the same problem for me

DNAlchemist avatar Sep 04 '19 11:09 DNAlchemist

Updating testcontainers from version 1.11.4 to version 1.12.0 fixed the same problem for me

But things still derive from the JUnit 4 TestRule 🤔

eggilbert avatar Sep 04 '19 14:09 eggilbert

Is there any update ? With v 1.12.5 the exclusion of junit dependency still results in class problems.

As a hint: at my work, I use a test container jdbc url for creating the datasource. And therefore, the docker container "run" creation is delegated to the test container driver itself. No explicit coding with @ClassRule and/or @Rule within my unit/integration tests. From that point of view, the dependency to junit4 does not make really sense to me.

rfelgent avatar Mar 05 '20 13:03 rfelgent

no updates since the last acknowledgement of the problem and the promise to target it for 2.x version.

@rfelgent As this issue is still open, this answer by @bsideup is still valid. The reasons are grounded in the way our code has been structured so far.

kiview avatar Mar 05 '20 14:03 kiview

With v 1.12.5 the exclusion of junit dependency still results in class problems.

You can create your wrapper maven project, exclude JUnit4 and provide necessary JUnit classes separately.

kostapc avatar Mar 05 '20 14:03 kostapc

Here's a workaround if no dependency on JUnit 4 is a must: create fake TestRule and Statement classes under your test root. Since GenericContainer doesn't really use them unless run by JUnit 4, this hack works fine.

package org.junit.rules;

@SuppressWarnings("unused")
public interface TestRule {
}

and

package org.junit.runners.model;

@SuppressWarnings("unused")
public class Statement {
}

detouched avatar May 07 '20 05:05 detouched

@detouched
Thanks. I also needed to include another empty class for my WebDriverContainer.

package org.junit.rules;
public class ExternalResource {
}

Was getting the following exception:


java.lang.NoClassDefFoundError: org/junit/rules/ExternalResource

	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
...
	at org.testcontainers.containers.Network.<clinit>(Network.java:22)
	at org.testcontainers.containers.BrowserWebDriverContainer.configure(BrowserWebDriverContainer.java:158)
	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:305)
	at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:300)

gtiwari333 avatar May 15 '20 20:05 gtiwari333

Is there any intention to fix this in the near future?

we switched to Junit5 and are explicitly excluding Junit4 from all dependencies to make sure people don't use it accidentally or out of habit

Same here :+1: We also excluded all JUnit 4 dependencies to be forced to use Jupiter.

pixelstuermer avatar May 18 '20 10:05 pixelstuermer

@pixelstuermer since this is not a major issue, atm we're focusing on other, bigger ones.

That said, we do plan to decouple the core from JUnit 4 in 2.0 (with probably an experimental API in 1.x that will allow the migration path), but it will take a bit of time.

bsideup avatar May 18 '20 12:05 bsideup

@pixelstuermer since this is not a major issue, atm we're focusing on other, bigger ones.

That said, we do plan to decouple the core from JUnit 4 in 2.0 (with probably an experimental API in 1.x that will allow the migration path), but it will take a bit of time.

OK thanks. Nice to hear that you will keep an eye on this 👍

pixelstuermer avatar May 27 '20 14:05 pixelstuermer

Any updates on this? It is really causing our team to struggle as there are junit 4 annotation but they don't work

ghahramani avatar Aug 20 '20 09:08 ghahramani

Same here folks, I hope this issue will be addressed soon. Migrated projects to the latest Spring Boot 2.4.2 where JUnit 4 is removed, so I hope we can get JUnit 5 support for testcontainers with JUnit 4 exluded.

20fps avatar Jan 26 '21 15:01 20fps

Since this requires some more fundamental changes to the existing codebase and architecture, we can't give an ETA on this feature. @bsideup already outlined some aspects in this issue and further inquiries won't expedite the development or release of this feature.

kiview avatar Jan 26 '21 19:01 kiview

Given the amount of duplicate issues asking for the same thing, I think this issue can be considered major?

gastaldi avatar Feb 04 '21 22:02 gastaldi

@gastaldi what makes it major? Does it break anything? Or blocks some usage?

Since GenericContainer depends on a class from JUnit 4, we can't easily remove the dependency, meaning that we either need to work on 2.0 or introduce a new API (something that we considered) that won't have said classes in the class hierarchy, so it can be excluded.

FWIW the current "problem" with JUnit 4 is that both JUnit 4 and JUnit 5 annotations are present and sometimes people use org.junit.Test instead of Jupiter's (unless we miss some other major issues with having JUnit 4 in our dependencies)

There are workarounds (see above), there are codestyle rules that prevents the usage of org.junit.* annotations. Given that, I struggle to agree that the issue is major, and that we should spend our (currently very reduced due to personal reasons) time on it and not on other major issues/features.

WDYT?

bsideup avatar Feb 04 '21 22:02 bsideup

It introduces security issues, for one. For JUnit 5 projects, JUnit 4 dependency offers extra attack surface.

lprimak avatar Feb 04 '21 23:02 lprimak

Hey @bsideup!

Since this introduces some API changes (major changes I believe) I agree this needs to be targeted to 2.0 (along with support for Podman, but that's another discussion 😄).

My use case is simple: we are planning to use TestContainers to bootstrap databases when running tests with Quarkus. Because our tests require JUnit 5, having the JUnit 4 JAR in the classpath may be confusing, but we can workaround it somehow.

See the full thread here: https://groups.google.com/g/quarkus-dev/c/EQpAI9kBrGk/m/URMR8OibBAAJ

gastaldi avatar Feb 04 '21 23:02 gastaldi

@lprimak please read that CVE before using the "security issues" argument. Just having a jar on your classpath does not introduce any security issues. Plus, that CVE was about the TemporaryFolder rule, and too permissive access that allows reading temporary files created by that rule by other users. This is hilarious to refer to it as a security risk of having JUnit 4 as a dependency 😂

bsideup avatar Feb 05 '21 07:02 bsideup

please read that CVE before using the "security issues" argument

oh come on now :) I am talking about attack surface, not that one particular issue.

lprimak avatar Feb 05 '21 07:02 lprimak

Guys come on, the Testcontainers team are working on this in their own free time, if you really want to expedite it, then you should help contribute to the project or get your company to sponsor them.

If not, if the issue is really critical to you, you can always use the workaround by @detouched here: https://github.com/testcontainers/testcontainers-java/issues/970#issuecomment-625044008

daniel-shuy avatar Feb 05 '21 07:02 daniel-shuy

@gastaldi There are no plans to support Podman in 2.0 (unless you know something that I don't, e.g. some generosity by Red Hat to join us as a Gold Sponsor and push the idea forward). As we previously communicated many times, Podman should provide an API that is compatible with Docker, as they claim they do.


My use case is simple: we are planning to use TestContainers to bootstrap databases when running tests with Quarkus.

Nice! Although I am afraid in this case even the new experimental API in 1.x won't help as you would expose the Testcontainers dependency to your users (otherwise you would just use the stub workaround), and excluding junit4 would make the end users struggle to understand why new GenericContainer fails the compilation for them when used in Quarkus... 2.0 is closer than ever (yes, I know that I said something similar 2 years ago, lol), so this period of junit4 dependency shouldn't be long :)

bsideup avatar Feb 05 '21 07:02 bsideup