skip to content

How do you make PER_CLASS the default test instance lifecycle for an entire JUnit 5 suite without annotating every class, using the junit.jupiter.testinstance.lifecycle.default configuration parameter — and what should make you hesitate?

level: middleimportance: nice to knowfreq 30%

answer

  1. junit.jupiter.testinstance.lifecycle.default
  2. values: per_method | per_class
  3. junit-platform.properties on the test classpath
  4. system property and launcher request override it
  5. @TestInstance on the class always wins

basics

~20 s

Set the JUnit Platform configuration parameter junit.jupiter.testinstance.lifecycle.default to per_class — via a junit-platform.properties file on the test classpath, a JVM system property of the same name, or a launcher discovery request parameter. An explicit @TestInstance on a class still wins. Hesitate because it silently removes isolation from classes nobody reviewed.

solid answer

~50 s

Jupiter reads a configuration parameter named `junit.jupiter.testinstance.lifecycle.default`, whose valid values are `per_method` (the built-in default) and `per_class`. You can supply it three ways, resolved in this precedence order: 1. a parameter passed in the `LauncherDiscoveryRequest` (highest), 2. a JVM system property of that exact name, 3. a `junit-platform.properties` file on the test classpath (lowest). An explicit `@TestInstance` on a class overrides all of them, so individual classes can still opt out. The reason to hesitate is that this flips a **safety default for code nobody re-read**. Every existing class silently gains shared mutable state; a field mutated by one test now reaches the next, and parallel methods now race on one instance. The change is invisible at the top of each test file, which is exactly where a reader looks. Prefer per-class annotations, or a composed annotation like `@IntegrationTest` that carries `@TestInstance`, so the intent stays local and greppable.

code

java · 1 line
java
junit.jupiter.testinstance.lifecycle.default = per_class

go deeper

for a junior

Know that the suite-wide default is settable through a JUnit configuration parameter and that a per-class annotation is the usual, more visible route.

for a middle

State the exact parameter name, its two values, and the supply channels, plus the rule that an explicit @TestInstance wins.

for a senior

Argue the trade-off: a global flip removes a safety default invisibly and changes existing classes retroactively; scope the decision and add detection if you take it.

for a principal

Treat it as a codebase-wide convention decision — justified mainly by language ergonomics — and require guardrails (composed annotations, randomized order, parallel runs in CI) before adopting it.

## The configuration parameter JUnit Jupiter's instance lifecycle has a built-in default of `PER_METHOD`, but that default is itself configurable through a **JUnit Platform configuration parameter**: ``` junit.jupiter.testinstance.lifecycle.default = per_class ``` Accepted values are `per_method` and `per_class` (case-insensitive; an unrecognized value logs a warning and falls back to `per_method`). It applies to every Jupiter test class in the run that does not carry its own `@TestInstance` annotation. ## How to supply it Configuration parameters are a general JUnit Platform mechanism, and this one is read like any other. In increasing order of precedence: 1. **`junit-platform.properties`** — a plain properties file on the **test runtime classpath** (conventionally in the test resources root). Committed to the repo, so it applies to IDE runs and CI alike. 2. **A JVM system property** with the identical name, e.g. `-Djunit.jupiter.testinstance.lifecycle.default=per_class` on the test JVM. 3. **A launcher discovery request parameter**, when something drives the platform programmatically through the `Launcher` API (`LauncherDiscoveryRequestBuilder.configurationParameter(...)`) — this is what a custom runner or IDE integration can pass. Higher entries in that list win over lower ones, and **an explicit `@TestInstance` on the class beats all of them**. That last point matters: the parameter changes only the *default*, never an expressed choice, so a class annotated `@TestInstance(Lifecycle.PER_METHOD)` keeps per-method isolation even in a `per_class` suite. The same properties file is where you would put other Jupiter defaults (display-name generator, parallel execution settings, and so on), so teams that already have one tend to reach for it here too. ## Why the parameter exists It was added mainly for **language and codebase ergonomics**. In Kotlin, a `static` `@BeforeAll` requires a `companion object` plus `@JvmStatic`, which is noisy in every class; a suite-wide `per_class` default removes that ceremony once. Some teams also standardize on per-class instances because their test style builds fixtures in `@BeforeAll` and keeps them immutable. The parameter lets those teams express the decision in one place instead of on hundreds of classes. ## Why you should hesitate The default it flips is a **safety default**, and the flip is invisible where it matters. - **Locality of reasoning disappears.** A reader opening `OrderServiceTest.java` sees no annotation and reasonably assumes fresh-instance isolation. The actual behavior lives in a properties file they may never have opened. Every future test author inherits an assumption they did not make. - **It changes existing classes retroactively.** Classes written under per-method assumptions — a field appended to in each test, a mock re-created in the constructor — suddenly share state. Some will fail loudly, which is fine; the dangerous ones pass while quietly becoming order-dependent. - **Parallelism gets harder everywhere at once.** If method-level concurrency is on (or gets turned on later), every class now shares one instance across threads. - **IDE and CI can diverge** if the parameter is supplied as a system property in the build rather than as a committed properties file: a test behaves one way locally and another in the pipeline, which is a miserable debugging session. ## What to do instead Prefer explicitness with the same amount of typing: - Annotate the classes that genuinely need it. It is one line, and it is the line a reviewer reads first. - Or create a **composed annotation** — `@TestInstance(Lifecycle.PER_CLASS)` plus whatever else your integration tests need — and apply that. The intent is now named (`@IntegrationTest`), greppable, and documented in one place, while still appearing at the top of every class that uses it. - If you do flip the global default (a Kotlin codebase is the strongest case), pair it with guardrails: randomized method order in CI, method-level parallel execution enabled somewhere so races surface, and a review habit of questioning mutable fields in test classes. ## Interview framing The knowledge check is the parameter name, its two values and the three supply channels; the judgment check is recognizing that a global lifecycle flip trades a per-class, visible decision for an invisible, suite-wide one. Saying "I know how, and here is why I would still annotate the handful of classes that need it" is the answer that lands.

  • If the properties file says per_class and a class is annotated @TestInstance(PER_METHOD), which wins?
    The annotation. The configuration parameter only supplies the default used when a class expresses no preference, so an explicit @TestInstance always takes precedence. That is what makes selective opt-out possible in either direction: a class can request per-class instances in a per-method suite, or insist on per-method isolation in a per-class suite.
  • Why is a committed junit-platform.properties usually preferable to passing the value as a system property on the test JVM?
    Because the properties file is on the test classpath, so IDE runs, local command-line runs and CI all resolve the same value, and the setting is version-controlled next to the tests. A system property supplied only by the build makes the IDE behave differently from the pipeline, producing failures that reproduce in one environment and not the other.

saying these in an interview costs you the question

  • Inventing a different parameter name or thinking the value is the enum constant PER_CLASS rather than per_class
  • Believing the parameter overrides an explicit @TestInstance annotation
  • Assuming it applies per package or per class rather than to the whole run
  • Flipping the global default without acknowledging that existing classes lose isolation retroactively
  • Expecting the properties file to be picked up from the main source set rather than the test classpath

context