7.3 test suite: inherit test libraries for integration test

I’d love to use the new test suite feature to define a new test suite for my integration tests. Previously, I defined a new source set and defined the configuration manually:

sourceSets {
    integrationTest {
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
    }
}

configurations {
    integrationTestImplementation.extendsFrom testImplementation
    integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

task integrationTest(type: Test) {
    description = 'Runs integration tests.'
    group = 'verification'

    testClassesDirs = sourceSets.integrationTest.output.classesDirs
    classpath = sourceSets.integrationTest.runtimeClasspath
    shouldRunAfter test
}

With the test suite plugin, this should look something like this:

testing {
    suites {
        integrationTest(JvmTestSuite) {
            testType = TestSuiteType.INTEGRATION_TEST
            dependencies {
                implementation project
                implementation XXX
            }

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

However, I don’t know how I can inherit the libraries from my test task. For example, my tests depend on AssertJ, and I’d love to inherit this dependency. I don’t mind depending on the actual test implementation, too, although that’s not necessary.

1 Like

I’d say same way

configurations {
    integrationTestImplementation.extendsFrom testImplementation
}
1 Like

I would go a different way. There’s no reason for testImplementation and integrationTestImplementation to have some kind of relationship. You need to know the names of the configurations and you have to drop out of the context of “testing” to define it.

Just explicitly say that “all JVM test suites have a dependency on XXX”:

testing {
    suites {
        withType(JvmTestSuite) {
            dependencies {
                implementation XXX
            }
        }
    }
}

Or duplicate the dependency into each test suite declaration.

We don’t support it now, but I don’t see why we couldn’t also allow:

testing {
    suites {
        configure(test, integrationTest) {
            dependencies {
                implementation "XXX"
            }
        }
    }
}
2 Likes

Thank you, that looks much nicer! However, follow-up issue: in my integration tests I don’t have access to transitive dependencies from my project. However, strangely, this isn’t an issue with the default test task.

Project :project-b has a dependency implementation project(':project-a'). A class that is declared in src/main/java of :project-a can be used from a test in src/test/java in “project-b”. However, this class is not available from within the integrationTest in src/integrationTest/java in “project-b”.

This is my test/integration test configuration:

testing {
    suites {
        test {
            useJUnitJupiter()
        }
        withType(JvmTestSuite) {
            dependencies {
                implementation '(some library)'
            }
        }
        integrationTest(JvmTestSuite) {
            testType = TestSuiteType.INTEGRATION_TEST
            dependencies {
                implementation project
            }
        }
    }
}

Where does the (implicit?) dependency for “test” come from? How can I add the same for “integrationTest”?

The implicit test dependency shows up as the first entry in “testCompileClasspath”. This entry is missing for “integrationTest”.

According to this integration test, the “test” task automatically gets transitive dependencies which appear in testCompileClassPath. This does not happen for non-default tasks, even if the “implementation project” dependency is given explicitly.

And I guess this is the code where “test” gets the magic dependencies:

Looks like I have to do that manually for integrationTest?

Looks like I have to do that manually for integrationTest?

Yes, this is by design. The default test suite preserves existing behavior of the test task, custom test suites will require these deps to be specified explicitly. We were hoping to enable flexibility with this, as some custom suites may not require these transitive deps.

OK, thank you for the clarification. I guess there are good reasons for this. However, I’d love to have a shortcut to (explicitly) copy the behaviour that “test” has, maybe something like “implementation transitive(project)”.

1 Like

Hey, I stepped across the same “problem”. Actually I have created a build file which uses the jvm-test-suite, but the dependencies coming from the project are actually not used, as the dependencies from the main project are not found in the step of compiling the integration test. Now I was asking myself how exactly I can make this happen.
Do you have a code snippet for me that should work? I am pretty new to Gradle and especially this plugin, but I would love to use it as probably this is the last step missing in my configuration.

I also recently ran into this problem, and I came up with a way to extend one test suite from another. The code below does exactly that. It is a little hacky in the way that it auto-discovers which configurations to extend with which ones, but it works. I am not sure how to determine which configurations belong to which test tasks without matching on the generated configuration names. This matches up the configuration names with the name of the test tasks removed.

I hope that the Gradle team will come up with a cleaner way to do this via the DSL.

In a util file in buildSrc:

import org.gradle.api.PolymorphicDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.NamedDomainObjectContainerScope
import org.gradle.kotlin.dsl.RegisteringDomainObjectDelegateProviderWithTypeAndAction
import org.gradle.kotlin.dsl.invoke
import org.gradle.testing.base.TestSuite

private fun extendFromConfigs(
    suiteToExtendFrom: JvmTestSuite,
    reuseSrc: Boolean = false,
    subAction: JvmTestSuite.() -> Unit
): JvmTestSuite.() -> Unit {
    var extendFromProject: Project? = null
    var extendFromConfigMap: Map<String, Configuration>? = null
    var extendFromTask: Test? = null

    suiteToExtendFrom.targets {
        all {
            extendFromTask = testTask.get()
            extendFromProject = extendFromTask!!.project
            extendFromConfigMap = configMapFor(extendFromProject!!, extendFromTask!!)
        }
    }

    return {
        useJUnitJupiter()

        dependencies {
            implementation(extendFromProject)
        }

        if (reuseSrc) {
            sources.java.setSrcDirs(suiteToExtendFrom.sources.java.srcDirs)
            sources.resources.srcDirs(suiteToExtendFrom.sources.resources.srcDirs)
        }

        targets {
            all {
                testTask {
                    val extendedConfigMap = configMapFor(this.project, this)
                    extendedConfigMap.forEach {(name, config) ->
                        config.extendsFrom(extendFromConfigMap!![name])
                    }
                    dependsOn(project.tasks.getByName("${extendFromTask!!.name}Classes"))
                    if (reuseSrc) {
                        this.testClassesDirs = extendFromTask!!.testClassesDirs
                    }
                }
            }
        }
        this.apply(subAction)
    }
}


private fun configMapFor(project: Project, testTask: Test): Map<String, Configuration> {
    val configMap = mutableMapOf<String, Configuration>()

    project.configurations.forEach { config ->
        if (config.name.startsWith(testTask.name)) {
            configMap[config.name.removePrefix(testTask.name)] = config
        }
    }

    return configMap
}


fun <T : TestSuite, C : PolymorphicDomainObjectContainer<T>> C.registeringExtended(
    suiteToExtend: JvmTestSuite,
    reuseSrc: Boolean = false,
    action: JvmTestSuite.() -> Unit
): RegisteringDomainObjectDelegateProviderWithTypeAndAction<out C, JvmTestSuite> =
    RegisteringDomainObjectDelegateProviderWithTypeAndAction.of(this, JvmTestSuite::class, extendFromConfigs(suiteToExtend, reuseSrc, action))

Then in build.gradle.kts, to run the same tests with different dependencies (registeringExtended gets “false” to not reuse the same test classes, but only share dependencies):

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

        val testOther by registeringExtended(test, true) {
            dependencies {
                implementation("other:depencency:1.0.0")
            }
        }

        project.tasks.named("check") {
            dependsOn(testOther)
        }
    }
}