Close

JUnit 5 - ExtensionContext Store - State Persistence

[Last Updated: Jan 3, 2026]

The JUnit 5 extension model provides a powerful mechanism for customizing test behavior. One key aspect is state persistence through the Store interface, which allows extensions to store and retrieve data across different phases of test execution.

Need for State Persistence

Extensions often need to maintain state between different lifecycle phases (before/after test execution) or share data across multiple test methods. Without proper state management, extensions would be limited to stateless operations. Since an extension instance is typically a singleton (created once per test class and reused for all test methods), using instance fields for per-test state would cause data to leak between tests. The Store mechanism addresses this need by providing a namespaced, scoped storage system that allows clean separation of state between different test methods while maintaining persistence within each test's lifecycle.

ExtensionContext Store Methods

Store getStore(Namespace namespace)

Retrieves a store associated with the given namespace within the current extension context's scope. This is the most commonly used method for extensions.

Store getStore(StoreScope scope,
               Namespace namespace)

Retrieves a store from a specific scope with the given namespace. This allows accessing stores from different levels of the test hierarchy.

Store Interface Key Methods

<V> V get(Object key,
                Class<V> requiredType)

Retrieves a value from the store by key with type safety.

<V> Object put(Object key,
                     V value)

Stores a value associated with the given key.

<V> V remove(Object key,
                   Class<V> requiredType)

Removes and returns a value from the store.

Understanding StoreScope

JUnit 5 provides different store scopes at various levels of the test hierarchy. The StoreScope enum defines these scopes:

  • StoreScope.LAUNCHER_SESSION: Store is available throughout the entire launcher session, allowing data sharing across multiple test executions. A Launcher Session is the highest level of test execution scope that spans multiple test classes. It's a entire JVM run.
  • StoreScope.EXECUTION_REQUEST: Store is available for the duration of the current execution request. An execution request can run one test or multiple tests.
    A Launcher session can run multiple execution requests.
  • StoreScope.EXTENSION_CONTEXT: Store is bound to the current extension context lifecycle (default scope when using getStore(Namespace))

These scopes can be accessed using getStore(StoreScope, Namespace) method introduced in JUnit 5.13.0.

Namespace Best Practices

Namespace provides isolation between different extensions. Best practices include:

  • Use unique namespace objects for each extension class
  • Namespace can be created using Namespace.create(Object...) with meaningful identifiers
  • Include extension class name and purpose in namespace to avoid collisions
  • Consider test context when creating namespaces for proper isolation

Cross-Test Data Sharing

Using getStore(StoreScope, Namespace) with appropriate scope allows extensions to share data between different levels of the test hierarchy. For example, using StoreScope.LAUNCHER_SESSION enables data persistence across multiple test classes or even multiple test runs within the same session.

Example

Extension Implementation

package com.logicbig.example;

import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

public class CounterExtension implements BeforeEachCallback, AfterEachCallback {

    private static final String COUNTER_KEY = "test-counter";
    private static final Namespace NAMESPACE = Namespace.create(
        CounterExtension.class.getName(), 
        "counter-state"
    );

    @Override
    public void beforeEach(ExtensionContext context) {
        // Using default EXTENSION_CONTEXT scope
        Store store = context.getStore(NAMESPACE);
        Integer counter = store.get(COUNTER_KEY, Integer.class);

        if (counter == null) {
            // First test in this context
            counter = 100;
            System.out.println("Initializing counter: " + counter);
        } else {
            System.out.println("Retrieved counter: " + counter);
        }

        // Increment and store
        counter++;
        store.put(COUNTER_KEY, counter);
        System.out.println("Stored counter: " + counter);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        Store store = context.getStore(NAMESPACE);
        Integer counter = store.get(COUNTER_KEY, Integer.class);
        System.out.println("Test completed. Final counter: " + counter);
    }
}

Test Class

package com.logicbig.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(CounterExtension.class)
public class ExtensionContextStoreTest {

    @Test
    void testMethod1() {
        System.out.println("Executing testMethod1");
        // CounterExtension will manage the counter
    }

    @Test
    void testMethod2() {
        System.out.println("Executing testMethod2");
        // CounterExtension will continue from previous test's counter
    }
}

Output

$ mvn test -Dtest=ExtensionContextStoreTest
[INFO] Scanning for projects...
[INFO]
[INFO] --------< com.logicbig.example:junit-5-extension-context-store >--------
[INFO] Building junit-5-extension-context-store 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-context-store ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-context-store\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-context-store ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-context-store ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-context-store\src\test\resources
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-context-store ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-context-store ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
Initializing counter: 100
Stored counter: 101
Executing testMethod1
Test completed. Final counter: 101
Initializing counter: 100
Stored counter: 101
Executing testMethod2
Test completed. Final counter: 101
[INFO] +--com.logicbig.example.ExtensionContextStoreTest - 0.084 ss
[INFO] | +-- [OK] testMethod1 - 0.047 ss
[INFO] | '-- [OK] testMethod2 - 0 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.488 s
[INFO] Finished at: 2026-01-02T15:03:57+08:00
[INFO] ------------------------------------------------------------------------

Enhanced Example with StoreScope

package com.logicbig.example;

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import static org.junit.jupiter.api.extension.ExtensionContext.Store;
import static org.junit.jupiter.api.extension.ExtensionContext.StoreScope;

public class SessionCounterExtension implements BeforeEachCallback {

    private static final Namespace SESSION_NAMESPACE = Namespace.create(
            SessionCounterExtension.class.getName(),
            "session-state"
    );

    @Override
    public void beforeEach(ExtensionContext context) {
        // Access LAUNCHER_SESSION scope for cross-session persistence
        Store sessionStore = context.getStore(StoreScope.LAUNCHER_SESSION, SESSION_NAMESPACE);
        Integer sessionCounter = sessionStore.get("session-counter", Integer.class);

        if (sessionCounter == null) {
            sessionCounter = 1000; // Higher starting value for session scope
            System.out.println("Initializing session counter: " + sessionCounter);
        } else {
            System.out.println("Retrieved session counter: " + sessionCounter);
        }

        // Increment session counter
        sessionCounter++;
        sessionStore.put("session-counter", sessionCounter);
        System.out.println("Updated session counter: " + sessionCounter);

        // Also demonstrate EXECUTION_REQUEST scope
        Store requestStore = context.getStore(StoreScope.EXECUTION_REQUEST, SESSION_NAMESPACE);
        Integer requestCounter = requestStore.get("request-counter", Integer.class);
        if (requestCounter == null) {
            requestCounter = 2000; // Different starting value for request scope
            System.out.println("initialized request counter: " + requestCounter);
        }else{
            System.out.println("Retrieved request counter "+requestCounter);
        }
        requestCounter++;
        requestStore.put("request-counter", requestCounter);
        System.out.println("Updated request counter: " + requestCounter);
    }
}

Test Class

package com.logicbig.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(SessionCounterExtension.class)
public class SessionCounterExtensionTest {

    @Test
    void sessionTest1() {
        System.out.println("Executing sessionTest1");
        // SessionCounterExtension will use LAUNCHER_SESSION scope
    }

    @Test
    void sessionTest2() {
        System.out.println("Executing sessionTest2");
        // SessionCounterExtension will continue from session store
    }
}

Output

$ mvn test -Dtest=SessionCounterExtensionTest
[INFO] Scanning for projects...
[INFO]
[INFO] --------< com.logicbig.example:junit-5-extension-context-store >--------
[INFO] Building junit-5-extension-context-store 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-context-store ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-context-store\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-context-store ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-context-store ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-context-store\src\test\resources
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-context-store ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-context-store ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
Initializing session counter: 1000
Updated session counter: 1001
initialized request counter: 2000
Updated request counter: 2001
Executing sessionTest1
Retrieved session counter: 1001
Updated session counter: 1002
Retrieved request counter 2001
Updated request counter: 2002
Executing sessionTest2
[INFO] +--com.logicbig.example.SessionCounterExtensionTest - 0.131 ss
[INFO] | +-- [OK] sessionTest1 - 0.068 ss
[INFO] | '-- [OK] sessionTest2 - 0.020 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.884 s
[INFO] Finished at: 2026-01-02T16:20:19+08:00
[INFO] ------------------------------------------------------------------------

The output demonstrates how extensions can persist state using the ExtensionContext Store. The CounterExtension shows per-test-context state management, while the SessionCounterExtension demonstrates cross-session data sharing using StoreScope. The successful incrementing of counters across different test methods confirms that the Store mechanism effectively maintains state at various scope levels, enabling extensions to share data and maintain persistence throughout different phases of test execution.

Example Project

Dependencies and Technologies Used:

  • junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
     Version Compatibility: 5.13.0 - 6.0.1Version List
    ×

    Version compatibilities of junit-jupiter-engine with this example:

    • 5.13.0
    • 5.13.1
    • 5.13.2
    • 5.13.3
    • 5.13.4
    • 5.14.0
    • 5.14.1
    • 6.0.0
    • 6.0.1

    Versions in green have been tested.

  • JDK 25
  • Maven 3.9.11

JUnit 5 - Extension Context Store Select All Download
  • junit-5-extension-context-store
    • src
      • test
        • java
          • com
            • logicbig
              • example
                • ExtensionContextStoreTest.java

    See Also