skip to content

You need every module in a large multi-project build to expose an identical integrationTest suite. How do you roll out the suite declaration so it's consistent and maintainable, and what pitfalls do you watch for?

level: seniorimportance: should knowfreq 30%

answer

  1. convention plugin, not copy-paste
  2. buildSrc / build-logic precompiled script
  3. suite's own dependencies { } block for accessors
  4. wire check inside the plugin
  5. managed JUnit version, avoid name collisions

basics

~20 s

Put the testing { suites { val integrationTest by registering(JvmTestSuite::class) { ... } } } declaration in a convention plugin (precompiled script plugin in buildSrc or a build-logic module) and apply that plugin to each subproject, so the suite is declared identically everywhere.

solid answer

~40 s

Don't copy the suite block into every `build.gradle.kts`. Author a **convention plugin** — a precompiled Kotlin script plugin like `my.java-conventions.gradle.kts` in `buildSrc` (or a `build-logic` included build) — that applies `java-library` and declares the `integrationTest` suite plus the `check` wiring. Each subproject just does `plugins { id("my.java-conventions") }`. This guarantees identical source-set layout, task names, and configuration names across modules, which is exactly what makes suites valuable at scale. Pitfalls: the `integrationTestImplementation` accessor may not be statically available in a convention plugin, so reference it via the suite's own `dependencies { }` block or the string form; remember each module still won't auto-join `check`, so wire it inside the plugin; and avoid suite names that collide with existing source sets. Keep the JUnit version managed via the suite (`useJUnitJupiter("5.x")`) so all modules align.

code

kotlin · 15 lines
kotlin
// buildSrc/src/main/kotlin/my.java-conventions.gradle.kts
plugins { id("java-library") }

testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("5.11.0")
            dependencies { implementation(project()) }
        }
    }
}
tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) }

// each module's build.gradle.kts
plugins { id("my.java-conventions") }

go deeper

for a junior

Recognise that the suite block can be centralised so you don't repeat it per module.

for a middle

Place the declaration in a convention plugin and apply it; mention the check-wiring and version-alignment needs.

for a senior

Detail buildSrc vs build-logic trade-offs, the accessor-availability pitfall, and using the suite's own dependencies block.

for a principal

Define the org-wide convention-plugin strategy, version-catalog integration, and migration path off legacy source-set boilerplate so all teams converge.

## The anti-pattern: copy-paste Pasting the same `testing { suites { } }` block into 30 `build.gradle.kts` files guarantees drift: someone bumps JUnit in one module, forgets the `check` wiring in another, mistypes a dir in a third. Suites only pay off when they are *uniform*. ## The pattern: a convention plugin Gradle's standard answer is a **precompiled script plugin** living in `buildSrc` or a separate `build-logic` included build (`includeBuild("build-logic")`). Example `buildSrc/src/main/kotlin/my.java-conventions.gradle.kts`: ```kotlin plugins { id("java-library") } testing { suites { val test by getting(JvmTestSuite::class) { useJUnitJupiter("5.11.0") } val integrationTest by registering(JvmTestSuite::class) { useJUnitJupiter("5.11.0") dependencies { implementation(project()) } } } } tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) } ``` Each module: `plugins { id("my.java-conventions") }`. Done — every module now has an identical `integrationTest` suite. ## Pitfalls to call out 1. **Accessor availability.** Inside a convention plugin, the typed `integrationTestImplementation(...)` accessor in the top-level `dependencies { }` block may not be generated. Prefer the suite's **own** `dependencies { }` block (as above) — it exposes `implementation(...)`, `runtimeOnly(...)` scoped to the suite without needing the prefixed accessor — or fall back to the quoted string form. 2. **`check` wiring per module.** Custom suites never auto-join `check`. Put the `dependsOn` inside the convention plugin so every module gets it once. 3. **Version alignment.** Pass the JUnit version to `useJUnitJupiter("5.x")` (or drive it from a version catalog) so modules don't diverge. 4. **Name collisions.** If a module already has a `src/integrationTest` source set from legacy build logic, registering the suite of the same name conflicts — migrate the legacy block out first. 5. **`buildSrc` vs included build.** `buildSrc` changes invalidate the whole build's configuration cache more aggressively; a separate `build-logic` included build is often preferred for large repos. ## Why this is the right altitude Centralising the suite declaration turns 'add integration tests' into a one-line plugin apply, makes the layout self-documenting, and lets you evolve all modules' test setup from a single file — the core organisational benefit of declaring suites declaratively.

  • Why declare suite dependencies inside the suite's own dependencies block rather than the top-level one in a convention plugin?
    The prefixed typed accessors (`integrationTestImplementation`) often aren't generated in a convention plugin's static context. The suite's `dependencies { }` block exposes scoped `implementation`/`runtimeOnly` that always resolve, avoiding the quoted-string workaround.
  • Where do you put the convention plugin, and what's the trade-off?
    Either `buildSrc` (simplest, but its changes invalidate config more broadly) or a separate `build-logic` included build via `includeBuild` (more isolation, preferred for large repos).

It's the difference between handing every team the same blueprint stamped from one master (convention plugin) versus letting each team sketch the floor plan from memory — the master guarantees every building has the same wiring.

saying these in an interview costs you the question

  • Recommending copy-pasting the suite block into every module — that defeats the consistency benefit and invites drift.
  • Forgetting that each module still needs `check` wiring, then claiming integration tests run in CI when they're never invoked.
  • Assuming the prefixed dependency accessor always exists in a convention plugin context.

context