0
0
JUnittesting~15 mins

TestInstance lifecycle per class in JUnit - Deep Dive

Choose your learning style9 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.