Gradle integration test suite depending on testImplementation dependencies

Viewed 1238

I am trying to migrate to the test suites introduced in Gradle 7.3. What I'd like to do is to add testImplementation dependencies to my integration tests.

testing {
    suites { 
        val test by getting(JvmTestSuite::class) { 
            useJUnitJupiter() 
        }

        val integrationTest by registering(JvmTestSuite::class) { 
            dependencies {
                implementation(project) // This adds dependencies to the prod code
                // What to add to automatically use testImplementation deps?
            }
            ...
 
        }
    }
}
4 Answers

You’d probably like to make the integrationTestImplementation configuration extend the testImplementation configuration – just like testImplementation already extends implementation by default. See also the docs on configuration inheritance.

Here’s a self-contained example (tested with Gradle 7.3.2):

plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

testing {
    suites { 
        val test by getting(JvmTestSuite::class) { 
            useJUnitJupiter() 
            dependencies {
                implementation("org.assertj:assertj-core:3.21.0")
            }
        }

        val integrationTest by registering(JvmTestSuite::class) { 
            dependencies {
                // TODO add any integration test only dependencies here
            }
        }
    }
}

// Here’s the bit that you’re after:
val integrationTestImplementation by configurations.getting {
    extendsFrom(configurations.testImplementation.get())
}

If you run ./gradlew dependencies --configuration integrationTestRuntimeClasspath with the configuration inheritance configured, then you’ll get the following output (abbreviated):

integrationTestRuntimeClasspath - Runtime classpath of source set 'integration test'.
+--- org.junit.jupiter:junit-jupiter:5.7.2
|    +--- org.junit:junit-bom:5.7.2
|    |    …
|    +--- org.junit.jupiter:junit-jupiter-api:5.7.2
|    |    …
|    +--- org.junit.jupiter:junit-jupiter-params:5.7.2
|    |    …
|    \--- org.junit.jupiter:junit-jupiter-engine:5.7.2
|         …
\--- org.assertj:assertj-core:3.21.0

However, if you run the same command without the configuration inheritance, then you’ll get the following output (abbreviated) – note the missing org.assertj:assertj-core:3.21.0 dependency:

integrationTestRuntimeClasspath - Runtime classpath of source set 'integration test'.
\--- org.junit.jupiter:junit-jupiter:5.7.2
     +--- org.junit:junit-bom:5.7.2
     |    …
     +--- org.junit.jupiter:junit-jupiter-api:5.7.2
     |    …
     +--- org.junit.jupiter:junit-jupiter-params:5.7.2
     |    …
     \--- org.junit.jupiter:junit-jupiter-engine:5.7.2

As requested in the answer comments, here’s additionally one way of making a test data class from the unit test suite available for integration testing:

sourceSets.named("integrationTest") {
    java {
        val sharedTestData = project.objects.sourceDirectorySet("testData",
                "Shared test data")
        sharedTestData.srcDir("src/test/java")
        sharedTestData.include("com/example/MyData.java")
        source(sharedTestData)
    }
}

I'm a Gradle Dev, currently working on this incubating feature.

One thing to keep in mind is that while currently the names of the configurations created for each test suite are based on the name of the corresponding sourceSet, which matches the name of the Test Suite itself, these names should be treated as an implementation detail. One of the main goals of Test Suites is to build a useful layer of abstraction over these details, and allow you to configure them without detailed knowledge of this sort of plumbing.

Wiring the configurations together with extendsFrom goes against this intent, as does any sort of retrieval by name, and should be seen as anti-patterns.

Better options include using suites.withType(JvmTestSuite).configureEach { suite -> ... } or creating a Closure containing the configuration you want and using it to configure the test suites of interest like this:

testing {
    suites {
        def applyMockitoAndJupiter = { suite -> 
            suite.useJUnitJupiter()
            suite.dependencies {
                implementation('org.mockito:mockito-junit-jupiter:4.6.1')
            }
        }

        /* This is the equivalent of:
            test {
                applyMockitoAndJupiter(this)
            }
         */
        test(applyMockitoAndJupiter) 

        /* This is the equivalent of:
            integrationTest(JvmTestSuite)
            applyMockitoAndJupiter(integrationTest)
         */
        integrationTest(JvmTestSuite, applyMockitoAndJupiter) 
    }
}

We're working to expand the functionality offered by Test Suites and each suite's dependencies block, as well as to make the docs more explicit about this point as well. You can see a preview (subject to further change) of this advice in a future version of the Gradle docs - you'll have to log in as guest.

I came up with following solution

testing {
    suites { 
        val test by getting(JvmTestSuite::class) { 
            useJUnitJupiter() 
        }

        val integrationTest by registering(JvmTestSuite::class) {
            useJUnitJupiter()

            dependencies {
                implementation(project)
                // add testImplementation dependencies
                configurations.testImplementation {
                    dependencies.forEach(::implementation)
                }
            }

            sources {
                java {
                    //...
                }
            }

            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                    }
                }
            }
        }
    }
}

This is what I used for not to rewrite the same dependencies twice for test and customTest tasks. Maybe it is not recommended as per @Tom Tresansky, but I find it quite convenient:

    configurations {
        customTestImplementation {
            extendsFrom testImplementation
        }
        customTestCompileOnly {
            extendsFrom testCompileOnly
        }
        customTestAnnotationProcessor {
            extendsFrom annotationProcessor
        }
    }
Related