skip to content

JVM Test Suites

The jvm-test-suite DSL that declares a whole suite — source set, task, and configurations at once — instead of hand-wiring each piece. Interviewers ask because it is the modern answer to 'how do you add integration tests?'

on this pageshow

explore

questions

25

Inside a JVM test suite's dependencies block, how do you give an integrationTest suite access to your project's own main/production classes?

level: juniorimportance: must knowfreq 60%

answer

  1. suite-local dependencies { } block
  2. implementation(project()) = this project
  3. custom suites don't inherit main classpath
  4. deps go to integrationTestImplementation

basics

~10 s

Add implementation(project()) inside the suite's dependencies { } block. The bare project() call refers to the current project, so the suite compiles and runs against main classes.

solid answer

~30 s

Each JVM test suite registered via the `jvm-test-suite` plugin has its own `dependencies { }` block. To let an `integrationTest` suite see the project's own production code you add `implementation(project())` there — `project()` with no path means *this* project. Unlike the built-in `test` suite, a custom suite does **not** automatically depend on the main source set's `implementation`/`runtimeOnly` classpath, so you must wire it explicitly. You can also pull in third-party libraries (`implementation(libs.someLib)`) in the same block; those resolve against the suite's dedicated `integrationTestImplementation` configuration, keeping them isolated from the `test` suite and from `main`.

code

kotlin · 10 lines
kotlin
testing {
  suites {
    val integrationTest by registering(JvmTestSuite::class) {
      dependencies {
        implementation(project())          // see main classes
        implementation(libs.junit.jupiter)
      }
    }
  }
}

go deeper

for a junior

Recall that implementation(project()) inside the suite's dependencies block gives access to main classes.

for a middle

Explain that the suite block maps to a dedicated integrationTestImplementation configuration and that custom suites don't inherit the main classpath.

for a senior

Contrast the special built-in test suite with custom suites, and discuss classpath isolation benefits of suite-specific configurations.

for a principal

Frame suite-local dependency blocks as the mechanism enforcing classpath isolation across test tiers org-wide, avoiding leaked deps between unit and integration tests.

## What the suite `dependencies` block is The `jvm-test-suite` plugin (applied automatically by the `java` plugin) exposes a `testing { suites { } }` container. Each suite you register — e.g. `val integrationTest by registering(JvmTestSuite::class)` — gets its **own** dependency block: ```kotlin testing { suites { val integrationTest by registering(JvmTestSuite::class) { dependencies { implementation(project()) } } } } ``` This block is a small typed DSL (`JvmComponentDependencies`) — it is **not** the top-level `dependencies { }` of the build. Methods like `implementation(...)`, `runtimeOnly(...)`, `compileOnly(...)`, and `annotationProcessor(...)` add to **suite-specific configurations** named after the suite: `integrationTestImplementation`, `integrationTestRuntimeOnly`, etc. ## Why `project()` is needed The built-in `test` suite is special: it inherits the main source set's compile/runtime classpath, so your tests see production classes for free. A **custom** suite is wired from scratch and gets no such inheritance. `implementation(project())` adds a dependency on the current project's outgoing API/runtime variants, so the suite can `import` and execute your main classes. `project()` (empty args) = the project the build script belongs to; `project(":other")` would target a different subproject. ## Pulling in shared libraries Inside the same block you reference version-catalog or coordinate dependencies just like a normal configuration: ```kotlin dependencies { implementation(project()) implementation(libs.junit.jupiter) runtimeOnly(libs.postgresql) } ``` Because these land in `integrationTestImplementation`, they are isolated from `testImplementation` — the unit-test suite never sees them, which keeps the two classpaths clean. ## Key takeaway Custom suites are isolated by default; `implementation(project())` is the explicit bridge from a suite to your own production code, and the suite block is where all of that suite's library deps live.

  • Why doesn't the custom integrationTest suite already see main classes the way the test suite does?
    The built-in `test` suite is special-cased to inherit the main source set's compile/runtime classpath. Custom suites are wired independently and inherit nothing, so you add `implementation(project())` explicitly.
  • What configuration does `implementation(...)` inside the integrationTest suite actually contribute to?
    `integrationTestImplementation` — a suite-specific configuration created by the plugin, isolated from `testImplementation` and from `main`'s `implementation`.

saying these in an interview costs you the question

  • Claiming `project()` needs the project path of the current project — bare `project()` already means the current project.
  • Assuming a custom suite inherits the main classpath automatically like the `test` suite does.

context

open as a page

What is the `testing { suites { } }` block in Gradle, and which plugin provides it?

level: juniorimportance: must knowfreq 55%

basics

~10 s

It is a DSL from the built-in jvm-test-suite plugin for declaring test suites (groups of tests). It is applied automatically by the java plugin and exposes a default suite named test.

open as a page

How do you declare a separate integrationTest test suite in a Gradle build using the JVM Test Suite plugin, and what does that one declaration give you for free?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Inside the testing.suites block, register a suite with val integrationTest by registering(JvmTestSuite::class). Gradle then auto-creates the integrationTest source set, the integrationTest task, and its dependency configurations — no manual source-set wiring needed.

open as a page

Inside a JVM test suite, how do you configure the underlying Test task (for example to call useJUnitPlatform()) using the targets DSL?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Go through the suite's targets: targets { all { testTask.configure { useJUnitPlatform() } } }. Each target exposes a testTask provider you configure, instead of touching the top-level test task directly.

open as a page

A teammate's custom integrationTest suite fails to compile with 'cannot find symbol' for classes from a library that the unit tests use fine. What's the likely cause and fix, given how suite dependencies work?

level: middleimportance: must knowfreq 40%

basics

~20 s

Custom suites don't inherit testImplementation. The library was declared only for the test suite, so the integration suite never saw it. Fix: declare it in the integrationTest suite's dependencies { }, or make integrationTestImplementation extend testImplementation.

open as a page

How does `useJUnitJupiter()` inside the default `test` suite replace manual `testImplementation`/`testRuntimeOnly` dependency wiring?

level: middleimportance: must knowfreq 50%

basics

~20 s

useJUnitJupiter() tells the suite to use JUnit 5. Gradle then adds the JUnit Jupiter API/engine and platform launcher dependencies to the suite's classpath and configures the test task's useJUnitPlatform(), so you don't add them by hand.

open as a page

When you register a suite named integrationTest, what exact names do the derived source set, task, and configurations get, and why does the suite name matter?

level: middleimportance: must knowfreq 50%

basics

~10 s

Everything is named after the suite. Source set: integrationTest (src/integrationTest/...). Task: integrationTest. Configurations: integrationTestImplementation, integrationTestRuntimeOnly, integrationTestCompileOnly, etc. So the suite name is the single key that derives all related artifacts.

open as a page

How do you make a single JVM test suite run on a specific JDK using a Java toolchain, and what does that actually do at execution time?

level: middleimportance: must knowfreq 45%

basics

~10 s

Set a JavaLanguageVersion on the target's Test task: testTask.configure { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } }. Gradle then runs that suite's tests on JDK 21, independent of the build JVM.

open as a page

In the JVM Test Suites DSL, what is a suite 'target' and how does it relate to the Test task that actually runs?

level: middleimportance: must knowfreq 55%

basics

~10 s

A target is a runnable execution of a suite. Each target owns a Test task (its testTask). Configuring the target's testTask is how you tweak how that suite actually runs.

open as a page

After declaring a custom `integrationTest` suite, how do you make it actually run as part of the build, and why isn't it included automatically?

level: middleimportance: must knowfreq 70%

basics

~10 s

Custom suites aren't wired into check by default. Add tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) } so running check (and thus build) also runs integration tests.

open as a page

What conventions does the default `test` suite assume — its source set, task name, and how it relates to `check`?

level: juniorimportance: should knowfreq 30%

basics

~10 s

The default test suite maps to the src/test/java (and resources) source set, produces the test task, and is wired into check so gradle check/build runs it.

open as a page

How do you reference or extend a JVM test suite's underlying SourceSet — for example to add an extra source directory or wire a custom configuration that isn't expressible in the suite's dependencies block?

level: middleimportance: should knowfreq 30%

basics

~10 s

Each suite exposes its SourceSet via sources (integrationTest.sources). Use it to add directories (java.srcDir(...)) or to grab generated configuration names like integrationTestImplementation for extendsFrom wiring not covered by the dependencies DSL.

open as a page

When wiring an integrationTest suite's dependencies, when would you depend on testFixtures(project()) instead of just project()?

level: middleimportance: should knowfreq 35%

basics

~10 s

project() gives the suite your production classes. testFixtures(project()) additionally exposes shared test-only helpers (builders, fakes, fixtures) published by the java-test-fixtures plugin, so multiple suites reuse them.

open as a page

When configuring suites, when do you use `getting` versus `registering` (or `getByName` vs `register`), and why does it matter for the default `test` suite?

level: middleimportance: should knowfreq 35%

basics

~10 s

Use getting/getByName to configure an existing suite like the default test suite. Use registering/register to create a brand-new suite. Calling register("test") fails because test already exists.

open as a page

The built-in `test` suite and a custom integrationTest suite are both JvmTestSuites. How do you reconfigure the built-in one through the same DSL, and how does that relate to registering a new suite?

level: middleimportance: should knowfreq 35%

basics

~20 s

Use getting instead of registering for the existing test suite: val test by getting(JvmTestSuite::class) { useJUnitJupiter() }. A new suite uses registering because it doesn't exist yet; the built-in test already exists, so you fetch and configure it.

open as a page

Contrast declaring an integrationTest JvmTestSuite with the old approach of manually creating a SourceSet and Test task. What boilerplate does the suite eliminate?

level: middleimportance: should knowfreq 55%

basics

~20 s

Manually you create a SourceSet, set its compile/runtime classpaths, and register a Test task pointing at it. Registering a JvmTestSuite does all of that automatically from the suite name, so you write one block instead of a dozen lines.

open as a page

How do you declare dependencies that are scoped to just one test suite, and how does that differ from adding them to the global testImplementation configuration?

level: middleimportance: should knowfreq 40%

basics

~10 s

Use the suite's dependencies { } block: integrationTest { dependencies { implementation(project()); implementation("org.testcontainers:junit-jupiter:1.19.0") } }. Those deps go only on that suite's classpath, not on test.

open as a page

How do you set per-suite test JVM runtime options — system properties, environment variables, JVM args, heap, parallel forks — and why do them through the suite's target rather than globally?

level: middleimportance: should knowfreq 30%

basics

~10 s

Configure the target's Test task: targets { all { testTask.configure { systemProperty("k","v"); environment("E","v"); jvmArgs("-XX:+EnableDynamicAgentLoading"); maxHeapSize="2g"; maxParallelForks=4 } } }. Doing it per-suite keeps each suite's runtime isolated.

open as a page

In a multi-module build, how do you make an integrationTest suite in one module depend on another module's production classes, and what are the trade-offs of doing so?

level: seniorimportance: should knowfreq 25%

basics

~10 s

Use implementation(project(":otherModule")) inside the suite's dependencies block. It adds a project dependency on that module's outgoing API/runtime variants. The trade-off is tighter build-graph coupling and recompilation when the dependency changes.

open as a page

The jvm-test-suite DSL is marked incubating and `useJUnitJupiter()` has a no-arg form. What practical trade-offs and version-pinning concerns does this raise for a production build?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Incubating means the API can change between Gradle versions, so wrap usage carefully. The no-arg useJUnitJupiter() picks a Gradle-managed default version; for reproducibility pin an explicit version or use a version-catalog Provider instead.

open as a page

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%

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.

open as a page

You want one test suite to run its tests across several JDK versions (a JDK matrix). How would you model that with suite targets and toolchains, and what are the trade-offs?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Give the suite multiple targets, each with a Test task pinned to a different JavaLanguageVersion launcher. Iterate the JDK list, register a target per version, and set javaLauncher on each target's testTask so the same tests run on each JDK.

open as a page

Within `targets { }`, when would you use `all { }` versus configuring a specific named target, and what does iterating with `all` guarantee?

level: seniorimportance: should knowfreq 35%

basics

~20 s

all { } applies configuration to every target the suite has now or gains later. Use a named target when you need settings specific to one execution. all guarantees future targets get the config too.

open as a page

You want unit tests to run before a custom integration suite during `check`, but running `gradle integrationTest` alone should NOT trigger unit tests. How do you express this, and what's the trap with `dependsOn`?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Use shouldRunAfter(test) on the integration target's testTask — a soft ordering. Don't use dependsOn(test), which would force unit tests to run every time you run integration tests.

open as a page

Across a multi-module monorepo, how would you standardize wiring multiple test suites (unit, integration, functional) into the lifecycle so CI runs the right suites at the right stage?

level: principalimportance: nice to knowfreq 18%

basics

~10 s

Put suite declaration, targets configuration, and check.dependsOn(...) wiring in a shared convention plugin applied to every module, so each module consistently exposes the same suites and CI stages can invoke them by task name.

open as a page