Bird
Raised Fist0
JUnittesting~15 mins

TestInstance lifecycle per class in JUnit - Deep Dive

Choose your learning style10 modes available
Overview - TestInstance lifecycle per class
What is it?
TestInstance lifecycle per class is a setting in JUnit that controls how test objects are created and reused during test execution. When set to per class, JUnit creates one instance of the test class and runs all test methods on that same instance. This differs from the default per-method lifecycle, where a new test object is created for each test method. It helps manage shared state and resource usage during testing.
Why it matters
Without controlling the test instance lifecycle, tests might run with unexpected side effects or inefficient resource use. If each test method creates a new object, setup can be slow and sharing data between tests is hard. Using per class lifecycle allows tests to share setup and state, making tests faster and enabling scenarios that require shared context. Without it, tests might be slower or harder to write correctly.
Where it fits
Before learning this, you should understand basic JUnit test structure and annotations like @Test and @BeforeEach. After this, you can learn about advanced test lifecycle annotations like @BeforeAll, @AfterAll, and how to manage test state safely. This concept fits into mastering JUnit test execution and writing efficient, maintainable tests.
Mental Model
Core Idea
JUnit's per class TestInstance lifecycle means one test class object is created and reused for all test methods, sharing state across them.
Think of it like...
It's like having one shared notebook for all your test notes instead of a fresh notebook for each test. You can write once and read later, but you must be careful not to mix notes from different tests.
┌─────────────────────────────┐
│ Test Class Instance (one)   │
│ ┌───────────────┐           │
│ │ testMethod1() │           │
│ └───────────────┘           │
│ ┌───────────────┐           │
│ │ testMethod2() │           │
│ └───────────────┘           │
│ ┌───────────────┐           │
│ │ testMethod3() │           │
│ └───────────────┘           │
└─────────────────────────────┘
Build-Up - 6 Steps
1
FoundationJUnit test class basics
🤔
Concept: Understanding how JUnit runs tests by creating test class instances.
JUnit by default creates a new instance of the test class for each test method. This means each test runs independently with a fresh object. For example, if you have three test methods, JUnit creates three separate objects, one per method.
Result
Each test method runs on a new object, so no shared state exists between tests unless static fields are used.
Knowing that JUnit creates a new object per test method explains why tests are isolated by default and why instance fields do not share values across tests.
2
FoundationTestInstance lifecycle concept
🤔
Concept: JUnit allows changing how test instances are created using TestInstance lifecycle annotations.
JUnit 5 introduced @TestInstance annotation to control lifecycle. It has two modes: PER_METHOD (default) and PER_CLASS. PER_METHOD creates a new test object per test method. PER_CLASS creates one test object for all test methods in the class.
Result
Using @TestInstance(PER_CLASS) means all test methods share the same test class instance.
Understanding this setting is key to controlling test object reuse and managing shared state in tests.
3
IntermediateUsing @TestInstance(PER_CLASS) annotation
🤔Before reading on: do you think @TestInstance(PER_CLASS) allows sharing instance variables safely between tests? Commit to your answer.
Concept: How to apply the PER_CLASS lifecycle and its effect on test execution.
Add @TestInstance(TestInstance.Lifecycle.PER_CLASS) on your test class. JUnit will create one instance and run all tests on it. This allows instance variables to keep values between tests. Example: @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { int counter = 0; @Test void test1() { counter++; } @Test void test2() { assertEquals(1, counter); } } Here, test2 sees the increment from test1.
Result
Tests share the same object, so instance fields keep their values across tests.
Knowing that instance fields persist across tests helps write tests that share setup or state but requires careful management to avoid test interference.
4
IntermediateImpact on lifecycle annotations
🤔Before reading on: do you think @BeforeAll can be non-static with PER_CLASS lifecycle? Commit to your answer.
Concept: How PER_CLASS lifecycle changes the rules for lifecycle methods like @BeforeAll and @AfterAll.
Normally, @BeforeAll and @AfterAll methods must be static because no test instance exists yet. With PER_CLASS lifecycle, JUnit creates the test instance before these methods run, so they can be instance methods (non-static). This simplifies setup and teardown code.
Result
You can write non-static @BeforeAll and @AfterAll methods when using PER_CLASS lifecycle.
Understanding this allows cleaner test setup code and avoids static method limitations.
5
AdvancedManaging shared state risks
🤔Before reading on: do you think sharing instance state between tests is always safe? Commit to your answer.
Concept: Sharing test instance state can cause tests to affect each other, leading to flaky tests.
When tests share the same object, changes in one test can affect others if instance variables are modified. This can cause tests to pass or fail unpredictably depending on execution order. To avoid this, reset shared state manually or design tests to be independent.
Result
Tests may become order-dependent and flaky if shared state is not managed carefully.
Knowing the risk of shared mutable state helps prevent subtle bugs and flaky tests in real projects.
6
ExpertPerformance and resource optimization
🤔Before reading on: does PER_CLASS lifecycle always improve test speed? Commit to your answer.
Concept: PER_CLASS lifecycle can improve performance by reusing expensive setup but may not always be faster.
Creating one test instance reduces object creation overhead and allows expensive setup in @BeforeAll to run once. However, if tests modify shared state, extra cleanup is needed, which can offset gains. Also, parallel test execution may be limited due to shared state. Use PER_CLASS lifecycle when setup cost is high and tests can safely share state.
Result
Potential faster tests with shared setup, but requires careful design to avoid side effects and concurrency issues.
Understanding tradeoffs between performance and test isolation guides choosing the right lifecycle for your project.
Under the Hood
JUnit uses reflection to create test class instances before running tests. In PER_METHOD lifecycle, it creates a new instance for each test method, ensuring isolation. In PER_CLASS lifecycle, it creates one instance per test class and reuses it for all test methods. Lifecycle annotations like @BeforeAll run after instance creation in PER_CLASS mode, allowing non-static methods. The test runner manages method invocation order and instance reuse internally.
Why designed this way?
JUnit was designed with PER_METHOD lifecycle to guarantee test isolation and avoid side effects. PER_CLASS lifecycle was added later to support scenarios needing shared state or expensive setup. Allowing non-static @BeforeAll methods in PER_CLASS mode simplifies test code. The design balances test isolation with flexibility and performance.
┌───────────────┐       ┌───────────────┐
│ PER_METHOD    │       │ PER_CLASS     │
├───────────────┤       ├───────────────┤
│ TestMethod 1  │       │ TestClass     │
│ (Instance 1)  │──────▶│ Instance      │
│ TestMethod 2  │       │               │
│ (Instance 2)  │       │ TestMethod 1  │
│ TestMethod 3  │       │ TestMethod 2  │
│ (Instance 3)  │       │ TestMethod 3  │
└───────────────┘       └───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does @TestInstance(PER_CLASS) guarantee tests run in any order? Commit yes or no.
Common Belief:Using PER_CLASS lifecycle means tests can run in any order safely because they share the same instance.
Tap to reveal reality
Reality:PER_CLASS lifecycle does not guarantee test order. Tests may run in any order, so shared state can cause unpredictable results if tests depend on order.
Why it matters:Assuming order safety leads to flaky tests that pass or fail depending on execution order, making debugging hard.
Quick: Can you use non-static @BeforeAll without @TestInstance(PER_CLASS)? Commit yes or no.
Common Belief:You can always write non-static @BeforeAll methods regardless of lifecycle.
Tap to reveal reality
Reality:Without PER_CLASS lifecycle, @BeforeAll must be static because no test instance exists yet.
Why it matters:Writing non-static @BeforeAll without PER_CLASS causes runtime errors and test failures.
Quick: Does PER_CLASS lifecycle always make tests faster? Commit yes or no.
Common Belief:PER_CLASS lifecycle always improves test speed by reusing the test instance.
Tap to reveal reality
Reality:PER_CLASS can improve speed but may slow tests if shared state requires extra cleanup or causes concurrency issues.
Why it matters:Blindly using PER_CLASS for speed can introduce flaky tests and harder maintenance.
Quick: Does sharing instance variables between tests mean tests are independent? Commit yes or no.
Common Belief:Sharing instance variables in PER_CLASS lifecycle keeps tests independent because they run on the same object.
Tap to reveal reality
Reality:Sharing instance variables can cause tests to depend on each other's side effects, breaking independence.
Why it matters:Misunderstanding this leads to tests that fail unpredictably and are hard to debug.
Expert Zone
1
PER_CLASS lifecycle allows non-static @BeforeAll/@AfterAll but requires careful design to avoid shared mutable state bugs.
2
Using PER_CLASS lifecycle can limit parallel test execution because tests share the same instance, affecting scalability.
3
Combining PER_CLASS lifecycle with dependency injection frameworks requires understanding object scopes to avoid lifecycle mismatches.
When NOT to use
Avoid PER_CLASS lifecycle when tests must be fully isolated or run in parallel without shared state. Use PER_METHOD lifecycle or container-based isolation instead.
Production Patterns
In large test suites, PER_CLASS lifecycle is used for expensive setup like database connections or web servers, combined with manual state reset between tests to balance speed and reliability.
Connections
Singleton pattern
Similar pattern of single instance reuse
Understanding PER_CLASS lifecycle is like the Singleton pattern in design: one instance shared across uses, requiring careful state management.
Test isolation principle
Opposite concept emphasizing independent tests
Knowing PER_CLASS lifecycle highlights the importance of test isolation to avoid side effects and flaky tests.
Database connection pooling
Builds-on shared resource reuse concept
PER_CLASS lifecycle's shared instance reuse is like connection pooling, optimizing resource use but needing careful management to avoid conflicts.
Common Pitfalls
#1Tests share mutable instance variables without resetting them.
Wrong approach:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { int count = 0; @Test void testA() { count++; } @Test void testB() { assertEquals(0, count); } }
Correct approach:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { int count = 0; @BeforeEach void reset() { count = 0; } @Test void testA() { count++; } @Test void testB() { assertEquals(0, count); } }
Root cause:Misunderstanding that instance variables persist across tests and must be reset to maintain test independence.
#2Using non-static @BeforeAll without PER_CLASS lifecycle.
Wrong approach:class MyTests { @BeforeAll void setup() { /* setup code */ } @Test void test() { } }
Correct approach:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { @BeforeAll void setup() { /* setup code */ } @Test void test() { } }
Root cause:Not knowing that @BeforeAll must be static unless PER_CLASS lifecycle is used.
#3Assuming PER_CLASS lifecycle guarantees test order independence.
Wrong approach:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { int value = 0; @Test void test1() { value = 5; } @Test void test2() { assertEquals(0, value); } }
Correct approach:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTests { int value = 0; @BeforeEach void reset() { value = 0; } @Test void test1() { value = 5; } @Test void test2() { assertEquals(0, value); } }
Root cause:Believing shared instance means tests run in a fixed order or are independent without explicit state reset.
Key Takeaways
JUnit's TestInstance lifecycle controls how test class objects are created and reused during testing.
PER_CLASS lifecycle creates one test instance for all test methods, enabling shared state but requiring careful management.
Using PER_CLASS allows non-static @BeforeAll and @AfterAll methods, simplifying setup and teardown.
Shared mutable state in PER_CLASS lifecycle can cause flaky tests if not reset properly between tests.
Choosing the right lifecycle balances test isolation, performance, and complexity in real-world testing.

Practice

(1/5)
1. What does the @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation do in a JUnit test class?
easy
A. Creates a single test instance shared by all test methods in the class.
B. Creates a new test instance before each test method runs.
C. Disables test instance creation for the class.
D. Runs tests in parallel threads automatically.

Solution

  1. Step 1: Understand the annotation purpose

    The @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation tells JUnit to create only one instance of the test class for all test methods.
  2. Step 2: Compare with default behavior

    By default, JUnit creates a new instance before each test method, but PER_CLASS changes this to reuse the same instance.
  3. Final Answer:

    Creates a single test instance shared by all test methods in the class. -> Option A
  4. Quick Check:

    PER_CLASS means one instance per class [OK]
Hint: PER_CLASS means one instance for all tests in class [OK]
Common Mistakes:
  • Confusing PER_CLASS with default PER_METHOD lifecycle
  • Thinking PER_CLASS disables instance creation
  • Assuming PER_CLASS runs tests in parallel
2. Which of the following is the correct syntax to set the test instance lifecycle to PER_CLASS in JUnit 5?
easy
A. @TestInstance(TestInstance.Lifecycle.PER_CLASS)
B. @TestInstance(Lifecycle.PER_CLASS)
C. @TestInstance(PER_CLASS)
D. @TestInstance(Lifecycle.PER_METHOD)

Solution

  1. Step 1: Recall the full annotation syntax

    The correct syntax uses the full enum path: @TestInstance(TestInstance.Lifecycle.PER_CLASS).
  2. Step 2: Check other options

    @TestInstance(Lifecycle.PER_CLASS) misses the TestInstance. prefix, @TestInstance(PER_CLASS) is incomplete, and @TestInstance(Lifecycle.PER_METHOD) uses PER_METHOD which is default, not PER_CLASS.
  3. Final Answer:

    @TestInstance(TestInstance.Lifecycle.PER_CLASS) -> Option A
  4. Quick Check:

    Full enum path required in annotation [OK]
Hint: Use full enum path: TestInstance.Lifecycle.PER_CLASS [OK]
Common Mistakes:
  • Omitting TestInstance prefix in annotation
  • Using PER_METHOD instead of PER_CLASS
  • Using incorrect enum names or syntax
3. Consider the following JUnit 5 test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS):
import org.junit.jupiter.api.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CounterTest {
    private int count = 0;

    @Test
    void testIncrement() {
        count++;
        Assertions.assertEquals(1, count);
    }

    @Test
    void testIncrementAgain() {
        count++;
        Assertions.assertEquals(2, count);
    }
}
What will be the result when running these tests?
medium
A. Tests fail with NullPointerException.
B. Both tests fail because count resets to 0 before each test.
C. First test passes, second test fails due to count reset.
D. Both tests pass because count is shared and increments correctly.

Solution

  1. Step 1: Understand PER_CLASS effect on instance

    With PER_CLASS, the same test instance is used for both tests, so the field count keeps its value between tests.
  2. Step 2: Trace the count value changes

    First test increments count from 0 to 1 and asserts 1. Second test increments count from 1 to 2 and asserts 2. Both assertions are correct.
  3. Final Answer:

    Both tests pass because count is shared and increments correctly. -> Option D
  4. Quick Check:

    Shared instance means shared state [OK]
Hint: PER_CLASS shares instance; state persists across tests [OK]
Common Mistakes:
  • Assuming count resets before each test
  • Expecting independent test instances
  • Confusing PER_CLASS with PER_METHOD lifecycle
4. Given this test class using PER_CLASS lifecycle:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SampleTest {
    private List<String> list = new ArrayList<>();

    @BeforeEach
    void setup() {
        list.clear();
        list.add("start");
    }

    @Test
    void testOne() {
        list.add("one");
        Assertions.assertEquals(2, list.size());
    }

    @Test
    void testTwo() {
        list.add("two");
        Assertions.assertEquals(2, list.size());
    }
}
If the list.clear() call is removed from setup(), what problem will occur?
medium
A. Tests will pass normally without issues.
B. Tests will fail because list keeps growing between tests.
C. Tests will throw NullPointerException.
D. JUnit will create new instances despite PER_CLASS.

Solution

  1. Step 1: Analyze list state without clearing

    Without list.clear(), the list keeps accumulating elements from previous tests because the same instance is reused.
  2. Step 2: Understand assertion impact

    Each test expects list size 2, but after first test, list size grows beyond 2, causing assertions to fail in subsequent tests.
  3. Final Answer:

    Tests will fail because list keeps growing between tests. -> Option B
  4. Quick Check:

    Shared state must be reset to avoid test interference [OK]
Hint: Clear shared state in setup to avoid test interference [OK]
Common Mistakes:
  • Forgetting to reset mutable fields in PER_CLASS tests
  • Assuming new instance per test with PER_CLASS
  • Ignoring side effects between tests
5. You want to optimize a test class by sharing expensive setup across tests using @TestInstance(TestInstance.Lifecycle.PER_CLASS). Which of the following is the best practice to avoid flaky tests?
hard
A. Remove all assertions to speed up tests.
B. Avoid using @BeforeEach and rely on constructor initialization only.
C. Reset all mutable shared state in a @BeforeEach method.
D. Use static fields to hold shared state instead of instance fields.

Solution

  1. Step 1: Understand shared state risks

    With PER_CLASS lifecycle, the same instance is reused, so mutable fields keep their values across tests, risking interference.
  2. Step 2: Apply best practice for test isolation

    Resetting mutable shared state in @BeforeEach ensures each test starts fresh, preventing flaky or dependent tests.
  3. Final Answer:

    Reset all mutable shared state in a @BeforeEach method. -> Option C
  4. Quick Check:

    Reset shared state before each test to avoid flaky tests [OK]
Hint: Always reset mutable state in @BeforeEach with PER_CLASS [OK]
Common Mistakes:
  • Relying only on constructor for setup with PER_CLASS
  • Using static fields causing global state issues
  • Ignoring test isolation principles
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard