TestInstance Annotation

@TestInstance is a class-level annotation in JUnit 5 that controls the lifecycle of test instances. It determines whether JUnit should create a fresh object for every test method or reuse a single object across an entire test class.

The annotation lives in org.junit.jupiter.api and accepts one of two values from the TestInstance.Lifecycle enum:

@TestInstance(TestInstance.Lifecycle.PER_METHOD)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)

Choosing between them involves a trade-off: PER_METHOD favors test isolation, while PER_CLASS simplifies shared setup and resource management.

Aspect PER_METHOD PER_CLASS
Test instances One instance per test method One instance per test class
Instance state Reset for every test method Shared by all test methods
@BeforeAll and @AfterAll Must be static May be static or non-static
Test isolation Strong Weaker
Typical use Independent unit tests Stateful or resource-heavy integration tests

Default behavior: PER_METHOD

PER_METHOD is JUnit 5’s default. JUnit creates a new test class instance before each test method, so mutable fields do not leak from one test into another.

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class DefaultLifecycleTest {
    private int counter = 0;

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

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

Although both methods increment the same field, each operates on its own instance. The second test therefore starts with counter equal to zero.

This behavior provides predictable isolation and is usually the best choice for unit tests, especially when the test class contains little or no mutable state.

Working with PER_CLASS

Setting the lifecycle to PER_CLASS tells JUnit to create one instance for the test class and use it for all test methods.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PerClassLifecycleTest {
    private int counter = 0;

    @Test
    void firstTest() {
        counter++;
    }

    @Test
    void secondTest() {
        counter++;
    }
}

Here, both tests operate on the same counter field. If firstTest() runs first, it changes the value from 0 to 1; secondTest() then changes it from 1 to 2.

However, JUnit does not guarantee the order in which test methods execute. A test that expects a particular counter value may therefore pass or fail depending on execution order. Shared state should not be used to create implicit dependencies between tests.

Non-static @BeforeAll and @AfterAll

One of the most practical advantages of PER_CLASS is that class-level lifecycle methods no longer have to be static.

In PER_METHOD mode, @BeforeAll runs before any test instance exists, so it must be declared as a static method. With PER_CLASS, JUnit creates the test instance before invoking @BeforeAll, allowing instance fields and non-static methods to be used.

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DatabaseLifecycleTest {
    private TestDatabase database;

    @BeforeAll
    void initializeDatabase() {
        database = new TestDatabase();
        database.start();
    }

    @AfterAll
    void closeDatabase() {
        database.stop();
    }

    @Test
    void databaseIsAvailable() {
        assert database.isRunning();
    }
}

This pattern avoids static fields and makes class-level resources easier to manage. It is especially useful for:

  • Database connections and test containers
  • HTTP clients and mock servers
  • Temporary directories
  • Cached fixtures
  • Thread pools and other shared infrastructure

The same resource is initialized once and closed once, while all test methods can access it through the shared instance.

Combining PER_CLASS with test isolation

Using one instance does not mean that all fields must be shared. A common approach is to keep expensive resources at the class level while resetting mutable test data before each method.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.ArrayList;
import java.util.List;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestServerTest {
    private TestServer server;
    private List<String> requests;

    @BeforeAll
    void startServer() {
        server = new TestServer();
        server.start();
    }

    @BeforeEach
    void resetRequests() {
        requests = new ArrayList<>();
    }

    @AfterAll
    void stopServer() {
        server.stop();
    }

    @Test
    void serverAcceptsRequests() {
        server.send("/health");
        assert server.isRunning();
    }
}

This design separates two different concerns:

  • The server is expensive and should exist for the whole class.
  • The request list represents mutable state and should be reset for every test.

Nested test classes

@TestInstance can also be applied to @Nested test classes. A nested class normally inherits the lifecycle configuration of its enclosing test class, but it can declare a different lifecycle when necessary.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class OuterTest {

    @Nested
    class InheritedLifecycleTest {
        @Test
        void runsWithPerClassLifecycle() {
            // Uses PER_CLASS.
        }
    }

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_METHOD)
    class OverriddenLifecycleTest {
        @Test
        void runsWithPerMethodLifecycle() {
            // Uses PER_METHOD.
        }
    }
}

This makes it possible to combine several lifecycle styles within the same test suite. Each nested class remains a separate test class, even when it inherits the enclosing class’s lifecycle setting.

Inheritance and meta-annotations

@TestInstance is inherited by subclasses. If a subclass does not declare its own value, it uses the lifecycle configured by its parent class. A subclass can explicitly override the inherited setting.

The annotation can also be used as a meta-annotation, which is helpful for teams that want a named lifecycle policy in their test suite.

import org.junit.jupiter.api.TestInstance;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public @interface SharedTestInstance {
}

A test class can then use the custom annotation:

@SharedTestInstance
class IntegrationTest {
    // Uses PER_CLASS lifecycle.
}

Practical considerations

Although PER_CLASS can reduce repeated object creation, performance is rarely the main reason to choose it. The more significant benefits are simpler lifecycle management and support for instance-level setup.

Several risks should be considered:

  • Shared mutable state: Changes made by one test remain visible to later tests in the same instance.
  • Execution order: JUnit does not guarantee method order, so assertions should not depend on another test running first.
  • Parallel execution: If parallel execution is enabled, mutable shared fields must be thread-safe or protected with appropriate synchronization.
  • Cleanup: Resources should always be released in @AfterAll, even when a test fails.
  • Accidental coupling: Shared fixtures can make tests harder to understand, isolate, and execute independently.

To keep a PER_CLASS suite reliable, prefer immutable class-level resources, reset mutable state in @BeforeEach, and avoid assertions based on the order or accumulated effects of other tests.

When to use each mode

Use the default PER_METHOD lifecycle when tests are independent, state is simple, or maximum isolation is important.

Choose PER_CLASS when the test class needs:

  • One shared resource across multiple tests
  • Non-static @BeforeAll or @AfterAll methods
  • Instance-level initialization performed by a test framework or extension
  • Constructor-based setup that should occur only once per class

@TestInstance gives