skip to content

Test Instance Lifecycle

Whether JUnit creates one test-class instance per method or per class, and everything that changes as a result. The single most-asked JUnit 5 lifecycle question.

on this pageshow

questions

5

In JUnit 5 (Jupiter), if a test class has three @Test methods, how many instances of that class does the framework create by default, and what is the reason for that design?

level: juniorimportance: must knowfreq 70%

answer

  1. new instance per @Test method
  2. PER_METHOD is the default
  3. constructor is not one-time setup
  4. @BeforeAll must be static here
  5. fields reset, statics and DBs do not

basics

~20 s

Three. Jupiter's default lifecycle is PER_METHOD: a brand-new instance of the test class is constructed before each @Test method. Instance fields therefore start fresh every time, so one test cannot leak state into another through fields.

solid answer

~50 s

By default JUnit Jupiter uses `@TestInstance(Lifecycle.PER_METHOD)`, so with three test methods the class is instantiated three times — one fresh instance per test method, each with its own copy of the instance fields. The point is isolation by construction: a test that mutates a field cannot influence the next test, so tests stay independent of execution order and can run in parallel without fighting over object state. Two consequences follow. First, `@BeforeAll` and `@AfterAll` must be `static` under this lifecycle, because there is no single instance they could belong to — they run once for the whole class, before any instance exists. Second, isolation stops at instance fields: `static` fields, singletons, databases, files and system properties are shared across all instances, so a new object per test does not automatically give you a clean world. The execution order per test is: construct the instance → `@BeforeEach` → `@Test` → `@AfterEach`.

code

java · 20 lines
java
class CounterLifecycleTest {

    private int counter = 0;

    @Test
    void first() {
        counter++;
        assertEquals(1, counter);
    }

    @Test
    void second() {
        counter++;
        assertEquals(1, counter); // passes: a different instance
    }

    @BeforeAll
    static void once() { // must be static under PER_METHOD
    }
}

go deeper

for a junior

State the fact cleanly — one new instance per @Test method, so instance fields are reset — and name the reason as test isolation.

for a middle

Add the mechanics: constructor runs per test, @BeforeAll must be static, and isolation covers instance fields only, not statics or external systems.

for a senior

Connect the lifecycle to order independence and parallel execution, and describe what you still have to reset yourself (DB, static caches, system properties).

for a principal

Frame it as the default that makes suites reproducible and shardable, and explain when you would deliberately opt a class out of it and what guardrails you would demand in return.

## What "test instance lifecycle" means A JUnit test class is an ordinary Java class, and its test methods are ordinary instance methods. To invoke one, the framework must have an object to invoke it on. "Test instance lifecycle" is simply the policy that decides **how often that object is created**: once per test method, or once per test class. JUnit Jupiter (the JUnit 5 programming model) expresses this policy with the `@TestInstance` annotation, whose two values are `Lifecycle.PER_METHOD` and `Lifecycle.PER_CLASS`. **`PER_METHOD` is the default**, and it is the answer to the question: three `@Test` methods means three constructions of the class. ## What actually happens for each test For every test method Jupiter performs roughly this sequence: 1. Construct a new instance of the test class (resolving any constructor parameters through registered `ParameterResolver` extensions — that is how `TestInfo`, `TestReporter` or a Spring/Mockito-injected collaborator can appear in the constructor). 2. Run extension instance post-processing (for example, field injection performed by an extension). 3. Run `@BeforeEach` methods on that instance (outermost first when inheritance or nesting is involved). 4. Run the `@Test` method. 5. Run `@AfterEach` methods on that instance. 6. Discard the instance — it becomes garbage. So the constructor runs once **per test**, not once per class. A common junior mistake is to treat the constructor as "setup that runs once"; it does not. ## Why the default is a fresh instance The motivation is **isolation by construction**. If every test gets its own object, then a field mutated by one test is invisible to the next one. That buys three practical properties: - **Order independence.** The suite produces the same result whether tests run in declaration order, alphabetically, in a randomized order, or split across shards. - **Safe parallelism.** Two test methods executing concurrently work on different objects, so there is no data race on instance fields. - **Cheap debuggability.** A failing test can be re-run alone and behave the same, because its starting state came from a constructor plus `@BeforeEach`, not from whatever the previous test left behind. JUnit 4 had the same rule (a new instance per test method), so this is not a JUnit 5 novelty — what JUnit 5 added is the *ability to opt out* with `PER_CLASS`. ## What the default does not isolate This is where interview answers separate. A fresh instance only resets **instance state**. Everything else survives: - `static` fields and static initializers — created once per class load, shared by every instance. - Singletons and application contexts (a cached Spring context, a connection pool, a Testcontainers container held in a static field). - External state: database rows, files on disk, message queues, `System.setProperty` changes, mutated `TimeZone`/`Locale` defaults, mocked static state. So "JUnit gives me a new instance, therefore my tests are independent" is only true for tests whose entire state lives in instance fields. Anything shared still needs explicit cleanup (`@AfterEach`, transaction rollback, truncating tables, restoring properties). ## Interaction with @BeforeAll / @AfterAll Under `PER_METHOD` there is no single instance that could own class-level setup, so `@BeforeAll` and `@AfterAll` methods **must be `static`** (Jupiter throws a configuration exception otherwise). They run exactly once for the class, before the first instance is constructed and after the last one is discarded, and because they are static they can only touch static state. This is precisely the pain point that motivates the alternative lifecycle: annotating the class `@TestInstance(Lifecycle.PER_CLASS)` makes Jupiter create **one** instance for all test methods, which in turn allows non-static `@BeforeAll`/`@AfterAll` and lets instance fields carry state across methods — at the cost of the isolation described above. ## How to demonstrate it in an interview The classic proof is a counter field incremented by each test: under the default lifecycle every test observes `1`, because each one runs against its own object. Change the class to `PER_CLASS` and the values become `1, 2, 3` in whatever order the methods ran. It is a two-minute experiment, and being able to describe its outcome confidently signals you have actually looked rather than memorized. ## Practical guidance Keep the default unless you have a concrete reason to change it. The default is what every reader of your test class assumes, it is the lifecycle that behaves correctly under parallel execution, and it makes each test reproducible in isolation. Reach for `PER_CLASS` deliberately and locally (one class at a time), not as a global convenience.

  • If a new instance is created for every test anyway, what is @BeforeEach still for?
    The constructor runs before Jupiter has finished preparing the instance, so anything supplied by extensions — injected fields, mocks initialized by an extension, a started container or a resolved temporary directory — is not reliably available in it. @BeforeEach runs after instance post-processing and after outer-class setup, and it participates in the inheritance and nesting order rules. It is also the only hook that can be inherited or contributed by extensions, which a constructor cannot be.
  • Does the per-method lifecycle reset static fields as well?
    No. A new instance only gives you fresh instance fields. Static fields belong to the class, are initialized once when the class is loaded, and are shared by every test instance and often by other test classes in the same JVM. Anything held statically — a cached client, a counter, a Testcontainers instance, a mocked singleton — must be reset explicitly, usually in @AfterEach or @AfterAll.
  • Was this different in JUnit 4?
    No, JUnit 4 also created a new instance of the test class for each test method, for the same isolation reason. What JUnit 5 added is the explicit @TestInstance annotation and the PER_CLASS option, which lets you opt into a single shared instance and into non-static @BeforeAll/@AfterAll methods.

A fresh test tube for each experiment instead of rinsing one out and hoping nothing is left in it — but the shared lab bench (statics, database, files) is never rinsed at all.

saying these in an interview costs you the question

  • Saying the class is instantiated once and reused, which is the PER_CLASS behavior, not the default
  • Treating the constructor as one-time setup that runs before all tests
  • Claiming a fresh instance makes tests independent of database, file or static state
  • Thinking @BeforeAll can be non-static under the default lifecycle
  • Believing JUnit 4 reused a single instance and JUnit 5 changed that

context

open as a page

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%

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.

open as a page

A JUnit 5 test class annotated @TestInstance(TestInstance.Lifecycle.PER_CLASS) passes when a single method is run in the IDE but fails when the whole class runs, and it fails intermittently once method-level parallel execution is switched on. What are the failure modes of holding mutable state in a shared test instance, and how do you contain them?

level: seniorimportance: should knowfreq 44%

basics

~20 s

With one instance for the class, mutations in one test are visible to the next, so tests become order-dependent (pass alone, fail together, or the reverse) and, under parallel methods, race on the same fields. Contain it: keep shared fields immutable after setup, reset mutable ones in @BeforeEach, and serialize the class with @Execution(SAME_THREAD) or @ResourceLock.

open as a page

Your JUnit 5 suite is slow because many test classes rebuild an expensive fixture — a started service, a large parsed document, a warmed client — before every test method. How would you decide between keeping the default one-instance-per-test-method lifecycle and moving classes to @TestInstance(PER_CLASS), and what guardrails would you require either way?

level: principalimportance: should knowfreq 30%

basics

~20 s

First check whether the cost is really per-instance; often it belongs in a fixture that can be built once and shared read-only. Move only classes whose expensive state is immutable after setup, keep everything mutable per test, and require guardrails: randomized method order, a run-each-test-alone job, and parallel execution enabled in CI.

open as a page

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%

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.

open as a page