What changes in a JUnit 5 test class when you annotate it with @TestInstance(TestInstance.Lifecycle.PER_CLASS), and why would you want that?
answer
- one instance for the whole class
- non-static @BeforeAll/@AfterAll allowed
- fields survive between tests
- non-static @MethodSource becomes legal
- Kotlin companion-object ceremony disappears
basics
~20 sJupiter creates one instance of the class for all its test methods instead of one per method. Instance fields then survive between tests, and @BeforeAll/@AfterAll (and non-static @MethodSource factory methods) may be non-static because there is now an instance to own them.
solid answer
~50 s`@TestInstance(Lifecycle.PER_CLASS)` switches the class from the default one-instance-per-test-method to **one instance for the whole class**. Three things follow: - **`@BeforeAll` and `@AfterAll` may be instance methods.** They still run once per class, but now on the shared instance, so they can initialize instance fields rather than statics. - **Instance fields persist across test methods**, which is exactly what you want for an expensive fixture built once (a started container, a parsed schema, a warmed client) and exactly what you do not want for anything a test mutates. - **Non-static factory methods become usable** for `@MethodSource`, and lifecycle code reads naturally in languages where `static` is awkward (Kotlin's companion objects, Groovy). The cost is that isolation is now your responsibility: tests can leak state into each other, they can become order-dependent, and parallel methods share one object. Apply it per class, deliberately, and keep mutable state out of the shared instance or reset it in `@BeforeEach`.
code
java · 21 lines@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ReportRendererTest {
private Template template; // shared, built once
private List<String> output; // mutable -> reset per test
@BeforeAll
void compileTemplate() { // no 'static' needed
template = Template.compile(loadLargeTemplate());
}
@BeforeEach
void freshOutput() {
output = new ArrayList<>();
}
@AfterAll
void releaseTemplate() {
template.close();
}
}go deeper
Know the core fact: one instance for the whole class, so @BeforeAll can be non-static and fields carry over between tests.
Add the motivations (expensive fixture, non-static @MethodSource, Kotlin ergonomics) and the isolation cost, and note that @BeforeAll timing is unchanged.
Talk about the failure modes it enables — order dependence, parallel sharing — and the discipline of keeping the shared instance read-only after setup.
Position it as a per-class opt-out from an isolation default, with explicit guardrails (randomized order runs, no mutable shared fields) rather than a suite-wide convenience.
## The annotation in one line `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` tells JUnit Jupiter to construct the annotated test class **once** and to invoke all of its test methods on that single object, replacing the default `PER_METHOD` behavior of building a fresh instance before every test. The annotation is placed on the test class (it is also `@Inherited`, so a subclass picks it up from a superclass, and it can be attached to a custom composed annotation such as your own `@IntegrationTest`). ## What concretely changes **1. Class-level hooks may be non-static.** Under the default lifecycle `@BeforeAll`/`@AfterAll` must be `static`, because they run when no instance exists. With `PER_CLASS` the single instance is created first, so Jupiter can call `@BeforeAll` on it. The timing is unchanged — once, before the first test; once, after the last — but the method can now write to instance fields: ```java @TestInstance(Lifecycle.PER_CLASS) class ParserTest { private Schema schema; // instance field, not static @BeforeAll void loadSchema() { schema = Schema.parse(bigFile()); } } ``` **2. Instance fields survive across test methods.** This is the whole point and the whole danger. Everything the class stores in a field is now shared by every test in it. **3. Non-static factory methods become legal for parameterized sources.** `@MethodSource` normally requires a `static` factory method; with `PER_CLASS` an instance method works, which matters when the arguments depend on fixture state built in setup. **4. `@Nested` inner classes can own class-level hooks.** A nested class cannot declare a `static` member in older Java language levels, so `PER_CLASS` on the nested class is the classic way to give it a `@BeforeAll`. **5. Language ergonomics.** In Kotlin, a `static` `@BeforeAll` requires a `companion object` plus `@JvmStatic`; `PER_CLASS` removes that ceremony entirely, which is why Kotlin codebases adopt it more often than Java ones. ## Why you would want it The honest motivations are narrow: - **Expensive one-time fixture that is not naturally static.** Starting a container, spinning up an embedded server, loading a large document, compiling a grammar. `PER_CLASS` lets you build it in `@BeforeAll` into a field and tear it down in `@AfterAll` without a static holder and without a manual "is it initialized yet" guard. - **Avoiding static state.** Ironically, `PER_CLASS` can *reduce* leakage: state in an instance field of a per-class instance dies when the class finishes, whereas a static field lives for the whole JVM and can bleed into other test classes. - **Fixture-dependent parameterized data**, via non-static `@MethodSource`. - **Kotlin/Groovy readability**, as above. What is **not** a good motivation is "my tests need to run in a sequence and pass state along". That is a design smell wearing a lifecycle annotation; see the trade-offs below. ## The trade-offs you must state - **Order dependence.** Once state persists, a test can pass only because an earlier test ran. The suite then breaks when someone reorders, filters (`--tests` / IDE single-test run), or shards it. The symptom is the classic "passes alone, fails in the suite" or its mirror image. - **Parallel execution.** If method-level parallelism is enabled, the shared instance is touched by multiple threads at once. Fields need to be effectively immutable, or the class needs `@Execution(SAME_THREAD)` or a `@ResourceLock`. - **Debug cost.** A failure now depends on the whole prefix of tests that ran before it, not just on the failing method. - **Silent drift.** Someone later adds a field and a mutation without noticing the annotation at the top of the file. This is why per-class adoption is safer than flipping the default suite-wide. ## Discipline that makes it safe Treat the shared instance as **read-only after `@BeforeAll`**. Fixture objects that are expensive and immutable (a parsed schema, a started server, a configured client) live in fields; anything a test mutates (a repository's contents, a captured list, counters) is created or reset in `@BeforeEach`. If you cannot cheaply reset it, do not share it. A useful check: run the class with a randomized method order, and run each test method individually in CI once in a while. Both surface accidental coupling that `PER_CLASS` made possible. ## Precedence An explicit `@TestInstance` on the class always wins over the suite-wide default configured through the `junit.jupiter.testinstance.lifecycle.default` configuration parameter, so a class can opt in even when the suite default is `per_method`, and can opt back out when the default has been flipped.
- Does PER_CLASS change when @BeforeAll runs, or only where it can live?Only where it can live. @BeforeAll still runs exactly once for the class, before the first test method, and @AfterAll once after the last one. The difference is that Jupiter now creates the single test instance first and invokes the hook on it, so the method may be non-static and may initialize instance fields.
- You want a container started once for a class. Would you prefer a static field with a static @BeforeAll, or PER_CLASS with an instance field?PER_CLASS with an instance field is usually cleaner: the fixture's lifetime is tied to the test class rather than to the JVM, so it cannot bleed into unrelated classes, and there is no static holder to reason about. A static field is preferable only when the resource must genuinely be shared across several test classes, in which case an extension or a singleton container pattern is the better expression of that intent.
- Can a single class mix PER_CLASS with parallel test execution?Yes, but the shared instance becomes shared mutable state across threads. Either keep the fields effectively immutable after @BeforeAll, or constrain the class with @Execution(ExecutionMode.SAME_THREAD), or guard the mutable resource with @ResourceLock so Jupiter serializes access. Doing none of these produces flaky, non-deterministic failures that are hard to reproduce.
saying these in an interview costs you the question
- Claiming PER_CLASS makes @BeforeAll run before each test or changes its timing
- Using it to pass state deliberately from one test method to the next and calling that a feature
- Assuming it is safe under parallel execution without any extra constraint
- Thinking the annotation must be repeated on every method or that it applies per package
- Believing it is required for any use of @MethodSource rather than only for non-static factory methods