skip to content

What changes in a JUnit 5 test class when you annotate it with @TestInstance(TestInstance.Lifecycle.PER_CLASS), and why would you want that?

level: middleimportance: must knowfreq 58%

answer

  1. one instance for the whole class
  2. non-static @BeforeAll/@AfterAll allowed
  3. fields survive between tests
  4. non-static @MethodSource becomes legal
  5. Kotlin companion-object ceremony disappears

basics

~20 s

Jupiter 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
java
@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

for a junior

Know the core fact: one instance for the whole class, so @BeforeAll can be non-static and fields carry over between tests.

for a middle

Add the motivations (expensive fixture, non-static @MethodSource, Kotlin ergonomics) and the isolation cost, and note that @BeforeAll timing is unchanged.

for a senior

Talk about the failure modes it enables — order dependence, parallel sharing — and the discipline of keeping the shared instance read-only after setup.

for a principal

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

context