skip to content

Walk through registering a custom source set (e.g. for integration tests) and wiring up a Test task to run it.

level: seniorimportance: should knowfreq 55%

answer

  1. sourceSets.creating / create()
  2. add main.output to classpaths
  3. extend testImplementation
  4. register Test task: testClassesDirs + classpath
  5. check.dependsOn + shouldRunAfter
  6. JVM Test Suite plugin = modern shortcut

basics

~10 s

Create 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 s

Registering 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 lines
kotlin
val 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

for a junior

Recognize that integration tests can live in their own source set; not expected to wire it manually.

for a middle

Create the source set and a Test task; know main.output must be added.

for a senior

Wire configurations via extendsFrom, set ordering with shouldRunAfter, hook into check, and contrast with the JVM Test Suite plugin.

for a principal

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.

context