skip to content

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%

answer

  1. testTask.configure { systemProperty / environment / jvmArgs }
  2. maxHeapSize, maxParallelForks, forkEvery
  3. per-suite avoids leakage + brittle withType conditionals
  4. values become task inputs (caching)
  5. maxParallelForks != --parallel

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.

solid answer

~40 s

Every suite target owns a `Test` task, and `Test` exposes the full runtime surface: `systemProperty`/`systemProperties`, `environment`, `jvmArgs`, `maxHeapSize`/`minHeapSize`, `maxParallelForks`, `forkEvery`, and the `filter`/`include`/`exclude` patterns. You set them inside `targets { all { testTask.configure { ... } } }`. Configuring per-suite is the point of the suite model: integration tests might need a 2 GB heap, a `spring.profiles.active=integration` system property, and `maxParallelForks = 1` (because they share a container), while unit tests stay lightweight with high parallelism. Setting these globally on the project-wide `test` task would either leak inappropriate settings into the wrong suite or force fragile `tasks.withType<Test>` conditionals. Going through the target keeps each suite self-describing and isolated, and the values become task inputs so changing them invalidates only that suite's up-to-date/cache state.

code

kotlin · 14 lines
kotlin
val integrationTest by registering(JvmTestSuite::class) {
    useJUnitJupiter()
    targets {
        all {
            testTask.configure {
                maxHeapSize = "2g"
                maxParallelForks = 1
                systemProperty("spring.profiles.active", "integration")
                environment("STAGE", "ci")
                jvmArgs("-XX:+EnableDynamicAgentLoading")
            }
        }
    }
}

go deeper

for a junior

Recall that you set systemProperty/jvmArgs/maxHeapSize on the target's testTask.

for a middle

Explain the runtime surface of Test, the difference between maxParallelForks and --parallel, and why per-suite isolation beats global config.

for a senior

Cover forkEvery/heap tuning trade-offs, values as task inputs for caching, and using providers for lazy wiring of dynamic values.

for a principal

Standardize test-JVM tuning across modules via convention plugins so heap/parallelism policies are consistent and observable.

## The Test task is the runtime knob When a suite runs, Gradle forks a JVM and executes the test classes. The `Test` task that each suite *target* owns is where you tune that forked JVM. Common settings: - **System properties**: `systemProperty("db.url", url)` or `systemProperties(mapOf(...))` -> passed as `-Dkey=value` to the test JVM (readable via `System.getProperty`). - **Environment variables**: `environment("STAGE", "ci")` -> visible via `System.getenv`. - **JVM args**: `jvmArgs("-XX:+EnableDynamicAgentLoading", "--enable-preview")`. - **Heap**: `minHeapSize = "512m"`, `maxHeapSize = "2g"`. - **Parallelism**: `maxParallelForks = 4` (parallel JVMs) and `forkEvery = 100` (restart a JVM every N classes to bound leakage). - **Selection**: `include`, `exclude`, `filter { includeTags("slow") }`. ## Per-suite, via the target ```kotlin testing { suites { val integrationTest by registering(JvmTestSuite::class) { useJUnitJupiter() targets { all { testTask.configure { maxHeapSize = "2g" maxParallelForks = 1 // shared Testcontainer systemProperty("spring.profiles.active", "integration") environment("TESTCONTAINERS_REUSE_ENABLE", "true") jvmArgs("-XX:+EnableDynamicAgentLoading") } } } } val test by getting(JvmTestSuite::class) { targets { all { testTask.configure { maxParallelForks = Runtime.getRuntime().availableProcessors() } } } } } } ``` Here unit tests run highly parallel and lightweight; integration tests get more heap and a single fork. The settings live next to each suite definition. ## Why not configure globally If you instead set everything on the global `test` task or via `tasks.withType<Test> { }`, you face two problems: 1. **Leakage**: a `maxHeapSize = "4g"` meant for integration tests slows or destabilizes unit tests, and a `spring.profiles.active=integration` would corrupt unit-test behavior. 2. **Conditionals**: you end up branching on `name == "integrationTest"` inside a `withType<Test>` block — brittle and hard to read. Going through the suite target keeps configuration **local, explicit, and isolated**. It also makes each value a proper **task input**: changing the integration suite's system property invalidates only that suite's up-to-date check and build-cache key, not the unit-test task's. ## Lazy/idiomatic notes - `testTask.configure { }` is lazy (configuration avoidance); prefer it over realizing the task. - Prefer providers for values that come from other tasks (e.g. a port from a started service) so wiring stays lazy and cacheable. - `maxParallelForks` parallelizes within a single `Test` task; it is independent of `--parallel` (which parallelizes across projects).

  • What is the difference between maxParallelForks and Gradle's --parallel flag?
    maxParallelForks parallelizes test execution within a single Test task by forking multiple JVMs. --parallel parallelizes task execution across independent projects. They operate at different levels and are independent.
  • Why might you set forkEvery on an integration suite?
    forkEvery restarts the test JVM after a set number of test classes, bounding memory leaks or accumulated static state in long integration runs at the cost of JVM startup overhead.
  • How do system properties set on the Test task reach the test code?
    They are passed as -Dkey=value to the forked JVM and read via System.getProperty("key") inside the tests.

saying these in an interview costs you the question

  • Setting integration-only heap/system properties on the global test task and leaking them into unit tests.
  • Confusing maxParallelForks with the --parallel project flag.
  • Branching on task name inside withType<Test> instead of configuring per-suite.

context