Walk through registering a custom source set (e.g. for integration tests) and wiring up a Test task to run it.
answer
- sourceSets.creating / create()
- add main.output to classpaths
- extend testImplementation
- register Test task: testClassesDirs + classpath
- check.dependsOn + shouldRunAfter
- JVM Test Suite plugin = modern shortcut
basics
~10 sCreate the source set with sourceSets { create("integrationTest") }, add main's output to its classpaths, register a Test task pointing at its testClassesDirs/classpath, and hook it into check.
solid answer
~40 sRegistering a custom source set is the idiomatic way to add an extra compilation unit such as integration tests. Steps: (1) `sourceSets { create("integrationTest") }` — this auto-creates `integrationTestImplementation`, `integrationTestRuntimeOnly`, `compileIntegrationTestJava`, etc. (2) Make `main` (and usually `test`) output and dependencies visible: `integrationTestImplementation` extends from `testImplementation`, and add `sourceSets.main.output` to its classpaths. (3) Register a `Test` task: set `testClassesDirs = sourceSets["integrationTest"].output.classesDirs` and `classpath = sourceSets["integrationTest"].runtimeClasspath`. (4) Wire it into the lifecycle with `tasks.named("check") { dependsOn(integrationTest) }`. Modern Gradle (7.4+) offers the JVM Test Suite plugin (`testing { suites { ... } }`) which automates all of this, but understanding the manual source-set route shows you grasp the underlying model.
code
kotlin · 14 linesval functionalTest by sourceSets.creating {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
configurations["functionalTestImplementation"].extendsFrom(configurations["testImplementation"])
val functionalTestTask = tasks.register<Test>("functionalTest") {
group = "verification"
testClassesDirs = functionalTest.output.classesDirs
classpath = functionalTest.runtimeClasspath
useJUnitPlatform()
shouldRunAfter("test")
}
tasks.check { dependsOn(functionalTestTask) }go deeper
Recognize that integration tests can live in their own source set; not expected to wire it manually.
Create the source set and a Test task; know main.output must be added.
Wire configurations via extendsFrom, set ordering with shouldRunAfter, hook into check, and contrast with the JVM Test Suite plugin.
Standardize a convention plugin so every module gets integration-test suites uniformly, and decide org policy on test-suite tooling.
## Why a custom source set Extra test kinds (integration, functional, JMH benchmarks) need their own source root, dependencies, and classpath, isolated from unit tests. A source set is exactly that unit, so modeling them as source sets gets you compilation tasks and configurations for free. ## Manual registration, step by step ```kotlin plugins { `java-library` } val integrationTest by sourceSets.creating { // Make production classes + main deps visible to integration tests compileClasspath += sourceSets.main.get().output runtimeClasspath += sourceSets.main.get().output } // Let integration tests reuse the test deps (JUnit, etc.) configurations["integrationTestImplementation"] .extendsFrom(configurations["testImplementation"]) configurations["integrationTestRuntimeOnly"] .extendsFrom(configurations["testRuntimeOnly"]) val integrationTestTask = tasks.register<Test>("integrationTest") { description = "Runs integration tests." group = "verification" testClassesDirs = integrationTest.output.classesDirs classpath = integrationTest.runtimeClasspath useJUnitPlatform() shouldRunAfter(tasks.named("test")) } tasks.named("check") { dependsOn(integrationTestTask) } ``` ### What each piece does - **`sourceSets.creating`** registers the set and triggers creation of its tasks and `integrationTest*` configurations. - **Adding `main.output`** puts compiled production classes on the integration-test classpath; extending `testImplementation` reuses unit-test libraries. - **`Test` task** is a standard task type; you must point its `testClassesDirs` and `classpath` at the new set because by convention only `test` is auto-wired to a runner. - **`shouldRunAfter`** is an ordering hint (not a hard dependency); **`check` dependsOn** makes the suite part of the verification lifecycle. ## The modern shortcut: JVM Test Suite plugin ```kotlin testing { suites { val integrationTest by registering(JvmTestSuite::class) { useJUnitJupiter() dependencies { implementation(project()) } targets { all { testTask.configure { shouldRunAfter(suites.named("test")) } } } } } } tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) } ``` This builds the source set, configurations, and `Test` task for you. In interviews, mention the suite plugin as the recommended path while showing you understand the manual mechanics underneath.
- Why do you set both `testClassesDirs` and `classpath` on the new `Test` task?`testClassesDirs` tells the runner where to discover compiled test classes; `classpath` is what's loaded at execution. Only the default `test` task is auto-wired, so a custom suite needs both pointed at its own source set's output and runtime classpath.
- What does the JVM Test Suite plugin give you over the manual approach?It auto-creates the source set, the `*Implementation` configurations, and the `Test` task, lets you pick the test framework declaratively, and wires sensible defaults — removing boilerplate and reducing mistakes like forgetting `main.output`.
- Why use `shouldRunAfter(test)` rather than `dependsOn(test)`?`shouldRunAfter` only orders the tasks when both are scheduled; it doesn't force `test` to run. `dependsOn` would make integration tests always trigger unit tests, coupling them and slowing targeted runs.
saying these in an interview costs you the question
- Forgetting to add `main.output` so integration tests can't see production classes.
- Using `dependsOn(test)` instead of `shouldRunAfter`, forcing unit tests every time.
- Hand-rolling a source set when the project could just use the JVM Test Suite plugin — fine to do manually, but you should know the modern option exists.