Skip to content

TestWatcher cannot access objects saved in ExtensionContext#Store before they are disposed #5151

@jannis-baratheon

Description

@jannis-baratheon

Version info

The problem occurs in 6.0.1 version. It used to work fine in 5.10.2 but then regressed in 5.11.0. A partial fix was implemented in #3944.

Steps to reproduce

Given below test:

package org.example.somepackage;

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

import static org.junit.jupiter.api.extension.ExtensionContext.*;

@ExtendWith(ExampleTest.ExampleExtension.class)
class ExampleTest {
    @Test
    void test() {
        assert false;
    }

    static class ExampleExtension implements BeforeEachCallback, AfterEachCallback, TestWatcher {
        private static final Namespace NAMESPACE = Namespace.create(ExampleExtension.class);

        @Override
        public void beforeEach(ExtensionContext context) {
            getStore(context).put("value", new ExtensionState("enigma"));
        }

        @Override
        public void afterEach(ExtensionContext context) throws Exception {
            System.out.println("afterEach: " + getStore(context).get("value"));
        }

        @Override
        public void testFailed(ExtensionContext context, Throwable cause) {
            System.out.println("testFailed: " + getStore(context).get("value"));
        }

        private static Store getStore(ExtensionContext context) {
            return context.getStore(NAMESPACE);
        }
    }

    private static class ExtensionState implements AutoCloseable {
        private String someString;

        public ExtensionState(String someString) {
            this.someString = someString;
        }

        @Override
        public void close() {
            someString = "DISPOSED";
        }

        @Override
        public String toString() {
            return "someString is '%s'".formatted(someString);
        }
    }
}

we get the following output (6.0.1):

afterEach: someString is 'ready to go'
testFailed: someString is 'DISPOSED'

with 5.10.2 it is:

afterEach: someString is 'ready to go'
testFailed: someString is 'ready to go'

with 5.11.0 it is:

afterEach: someString is 'ready to go'
Nov 12, 2025 9:52:37 PM org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor lambda$invokeTestWatchers$3
WARNING: Failed to invoke TestWatcher [org.example.somepackage.ExampleTest$ExampleExtension] for method [org.example.somepackage.ExampleTest#test()] with display name [test()]
org.junit.jupiter.api.extension.ExtensionContextException: A NamespacedHierarchicalStore cannot be modified or queried after it has been closed

Expected result

The expected result is that the TestWatcher callbacks can access ExtensionContext#Store before it is disposed - as it was in 5.10.2.

I can see that there was a shot at fixing this problem in #3944 but it was only partially successful.

The other problem is that we cannot remove items from the store in TestWatcher callbacks because we get A NamespacedHierarchicalStore cannot be modified or queried after it has been closed.

Real life scenario

I have an extension which sets up Selenium's WebDriver in BeforeEachCallback. After the tests are done with the WebDriver I destroy it TestWatcher callbacks. In case a test failed I need to gather some additional information (e.g. screenshot, some dumps, etc.) before the cleanup. It used to work before 5.11.0 and now it's impossible to do correctly IMO.

The workaround I have for now is to move the cleanup code to the AfterEachCallback. The problem is that in AfterEach we do not know if the test failed or not. This is my current workaround but it seems fragile and doesn't convince me:

    @Override
    public void afterEach(@Nonnull ExtensionContext context) {
        var testFailed = context.getExecutionException().isPresent();

        if (testFailed) {
            gatherInformation(context);
        }

        cleanup(context);
    }

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions