skip to content

JUnit's platform reads settings such as parallel test execution and the default test-instance lifecycle from configuration parameters. Where can those parameters be supplied, and which source wins when two of them disagree?

level: middleimportance: should knowfreq 42%

answer

  1. request params > system properties > junit-platform.properties
  2. file lives at classpath root: src/test/resources/
  3. parallel.enabled is only the master switch
  4. testinstance.lifecycle.default=per_class
  5. Surefire <configurationParameters> outranks -D

basics

~20 s

Configuration parameters are simple key/value settings. They can come from the LauncherDiscoveryRequest, a JVM system property, or a junit-platform.properties file at the root of the classpath — and that is also the precedence order, request first, file last.

solid answer

~40 s

JUnit Platform configuration parameters are flat key/value settings read at discovery and execution time — for example `junit.jupiter.execution.parallel.enabled`, `junit.jupiter.testinstance.lifecycle.default`, `junit.jupiter.displayname.generator.default`. Three sources, checked in this order: 1. Parameters passed directly in the `LauncherDiscoveryRequest` — which is what a build tool sets on your behalf (Surefire's `<configurationParameters>`, the ConsoleLauncher's `--config`). 2. JVM system properties (`-Djunit.jupiter.execution.parallel.enabled=true`, or Gradle's `systemProperty`). 3. A `junit-platform.properties` file at the **root of the classpath**, conventionally `src/test/resources/junit-platform.properties`. First match wins, so a system property overrides the file and a build-tool-supplied parameter overrides both. That layering is what lets a properties file hold the team default while CI overrides one key per job. The file is the durable, reviewable place for defaults; ad-hoc overrides belong on the command line.

code

properties · 6 lines
properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = dynamic
junit.jupiter.displayname.generator.default = \
  org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

go deeper

for a junior

Know that the settings file is junit-platform.properties in test resources and name one or two parameters you have used.

for a middle

State all three sources and the precedence order, and explain that the parallel switch needs an accompanying mode.

for a senior

Reason about where a setting should live so IDE, build tool and CI agree, and about the blast radius of global settings like per_class lifecycle or auto-detected extensions.

for a principal

Treat these as suite-wide policy: which defaults are safe to standardise across modules, how parallelism interacts with shared fixtures and resource locks, and how to prevent per-job overrides from drifting into undocumented behaviour.

## What a configuration parameter is A configuration parameter is a string key mapped to a string value, read by the JUnit Platform and by engines during discovery and execution. Unlike annotations, they are external to the code, so they can differ per environment without editing tests. Extensions can read them too, through `ExtensionContext.getConfigurationParameter(key)`, which is how custom extensions get configurable behaviour. ## The three sources and their order When anything asks for a parameter, JUnit looks in this order and returns the first hit: 1. **Explicit parameters in the `LauncherDiscoveryRequest`.** Programmatic callers set them via `LauncherDiscoveryRequestBuilder.configurationParameter(...)`. In practice the build tool does this for you: Maven Surefire's `<configurationParameters>` block, and the ConsoleLauncher's `--config key=value`. 2. **JVM system properties.** `-Djunit.jupiter.execution.parallel.enabled=true` on the test JVM. In Gradle this is `test { systemProperty 'junit.jupiter.execution.parallel.enabled', 'true' }`. 3. **The `junit-platform.properties` file** at the *root of the classpath*. Conventionally `src/test/resources/junit-platform.properties`. First match wins. That ordering is deliberate: the file is the checked-in team default, the system property is the per-run override, and the request-level parameter is the tool's final say. A subtlety worth knowing: the file is looked up on the classpath root, so if two modules or two jars each ship a `junit-platform.properties`, which one is found depends on classpath order. Keep exactly one per test runtime, in the module's own test resources. ## The parameters you will actually meet **Parallel execution** (Jupiter): - `junit.jupiter.execution.parallel.enabled=true` — the master switch; without it nothing runs in parallel. - `junit.jupiter.execution.parallel.mode.default=concurrent|same_thread` — default for methods. - `junit.jupiter.execution.parallel.mode.classes.default=concurrent|same_thread` — default for top-level classes. - `junit.jupiter.execution.parallel.config.strategy=dynamic|fixed|custom` with `...config.fixed.parallelism=N` or `...config.dynamic.factor=1.0`. Enabling the switch alone changes nothing unless a mode is set to `concurrent` or tests carry `@Execution(CONCURRENT)`. Synchronisation between parallel tests is expressed with `@ResourceLock`, not with parameters. **Test instance lifecycle:** `junit.jupiter.testinstance.lifecycle.default=per_class` makes Jupiter reuse one instance per class instead of creating a fresh one per test method, which also allows non-static `@BeforeAll`. Setting this globally changes the isolation guarantees of every existing test in the module, so it is a decision, not a tweak. **Display names:** `junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores` turns `finds_user_by_id` into "finds user by id" across the whole suite. **Extension auto-detection:** `junit.jupiter.extensions.autodetection.enabled=true` registers every `Extension` found via the ServiceLoader, instead of requiring `@ExtendWith` on each test. **Condition deactivation:** `junit.jupiter.conditions.deactivate=org.junit.*DisabledCondition` temporarily runs tests that `@Disabled` would skip — useful for a one-off "run everything" job. **Method ordering:** `junit.jupiter.testmethod.order.default` sets a default `MethodOrderer` for the suite. ## How build tools pass them Gradle has no dedicated DSL for configuration parameters, so you use system properties: ```groovy test { useJUnitPlatform() systemProperty 'junit.jupiter.execution.parallel.enabled', 'true' } ``` Maven Surefire has a first-class block that becomes request-level parameters: ```xml <configuration> <properties> <configurationParameters> junit.jupiter.execution.parallel.enabled = true </configurationParameters> </properties> </configuration> ``` Because Surefire's block is request-level, it beats both the system property and the file — which surprises people who set a system property and see it ignored. ## Practical guidance Put stable, team-wide defaults in `junit-platform.properties` where they are version-controlled, reviewable, and picked up identically by the IDE, the build tool and the ConsoleLauncher. Keep the command line for genuinely per-run overrides (a CI job that force-enables parallelism, a nightly run that deactivates the disabled-condition). When behaviour differs between a developer's IDE run and CI, the precedence order is the first thing to check: the IDE usually only sees the file, while CI may also be injecting a system property or a Surefire parameter.

  • You set junit.jupiter.execution.parallel.enabled=true and nothing runs in parallel. Why?
    The flag only turns the feature on; it does not choose a mode. Unless `junit.jupiter.execution.parallel.mode.default` or `...mode.classes.default` is set to `concurrent`, or the tests carry `@Execution(CONCURRENT)`, everything still runs in the same thread. The usual first step is concurrent classes with same-thread methods, so each class keeps its internal ordering while classes overlap.
  • A setting works in CI but not when a developer runs the test from the IDE. How does the precedence order explain that?
    IDEs typically launch the platform with only the classpath, so they see `junit-platform.properties` and any system properties in the run configuration — but nothing the build tool would have injected. If the value lives in Surefire's `<configurationParameters>` or a Gradle `systemProperty`, the IDE never receives it. Moving the setting into the properties file makes all three entry points agree.

saying these in an interview costs you the question

  • Thinking junit-platform.properties can live anywhere in the project rather than at the classpath root
  • Believing parallel.enabled alone makes tests run concurrently
  • Assuming a -D system property always overrides the build tool's configuration parameters
  • Confusing configuration parameters with Gradle/Maven plugin settings
  • Flipping the default lifecycle to per_class without considering shared state between tests

context