1

In most cases when I consider creating an extension, I run into the same problem: How to store mutable objects? Here is my problem: Let's consider that my extension provides a parameter resolver which provides a mutable object to the test. Let's say the object has methods to change the configuration. A naive implementation based on the JUnit 5 User Guide and Javadoc could look like this:

public class MyExtension implements ParameterResolver {
  private static final Namespace NAMESPACE = Namespace.create(MyExtension.class);

  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return parameterContext.getParameter().getType() == MyMutableType.class;
  }

  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return extensionContext.getStore(NAMESPACE).getOrComputeIfAbsent(MyMutableType.class, __ -> new MyMutableType());
  }
}

Unfortunately, this implementation is broken for the following test class.

@ExtendWith(MyExtension.class)
final class MyTest {
  @BeforeAll
  static void init(MyMutableType resolvedObject) {
  }

  @Test
  void test1(MyMutableType resolvedObject) {
    resolvedObject.changeSomeConfig();
    ...
  }

  @Test
  void test2(MyMutableType resolvedObject) {
    // resolvedObject might be affected by changed configuration in test1().
    ...
  }
}

Till this day, I couldn't find a good solution. Is there a guide by JUnit how this is supposed to work. I couldn't find anything. To solve this issue, I see two approaches. Neither of them seems to work well with JUnit's API.

One approach is to forbid using my parameter resolver when the ExtensionContext is not specific to a single test. Unfortunately, I couldn't find any reliable way to check that. I can check for annotations like @BeforeAll but that is more an estimation then a reliable check.

The second approach is to copy the object when we enter a more specific ExtensionContext. Alternatively, I could set a flag which prevents further modification and provides meaningfull error messages. However, such implementation is far from straightforward and looks more like misusing the API. Beside that, the implementation might be too strict by using the copyOperator when not actually necessary.

<T> T getOrCompute(ExtensionContext extensionContext, Object key, Supplier<T> factory, UnaryOperator<T> copyOperator) {
  Store store = extensionContext.getStore(NAMESPACE);
  // Using remove() because it is the only method ignoring ancestors
  Object previous = store.remove(key);
  if (previous == null) {
    T fromAncestor = (T) store.get(key);
    if (fromAncestor == null) {
      T result = factory.get();
      store.put(key, result);
      return result;
    }
    else {
      T result = copyOperator.apply(fromAncestor);
      store.put(key, result);
      return result;
    }
  }
  else {
    store.put(key, previous);
    return (T) previous;
  }
}

I'm wondering if I'm missing something important or if JUnit just doesn't have a meaningful way to handle mutable state in extensions.

JojOatXGME
  • 3,023
  • 2
  • 25
  • 41
  • It looks to me like your spec has a contradiction in it. Using BeforeAll suggests your parameter should be the same for all tests but each test should have an instance of its own. Only one is possible. Can you clarify? – johanneslink Nov 01 '20 at 04:58
  • @johanneslink This is one way of seeing it. Then, the problem is that I – the author of the extension – cannot validate how it is used. If I don't want that my *parameter resolver* is used in `@BeforeAll`, I cannot prevent it. – JojOatXGME Nov 01 '20 at 10:54
  • @johanneslink Another viewpoint is that in some cases, I could actually support such usage by copying the object (as described above) but the implementation looks very hacky. Whichever approach I take, the API seems to miss functionality to properly implement either of them. At least if I want to fail eraly with a meaningful error message when my extension is misused. – JojOatXGME Nov 01 '20 at 11:13
  • (Maybe I should have split it into two questios. I put it into one questions because I'm currently wondering how it is intended to be used, regardless of the implemented approach.) – JojOatXGME Nov 01 '20 at 11:26
  • You can check the type of extension context and refuse to resolve parameter if you’re not in a test execution context. – johanneslink Nov 01 '20 at 14:14
  • @johanneslink Do you mean `instanceof MethodExtensionContext`? To me, it dosen't look as this class is part of the public API. Anyway, I decided to split the question into two. [I created the first one here.](https://stackoverflow.com/q/64635543/4967497) You can post your answer there. I will create the second one later. – JojOatXGME Nov 01 '20 at 18:43
  • You are right MEC is not published. I see two options: "context.getTestMethod().isPresent()" could do the trick or you open an issue requesting some kind of "context.isInMethodContext()" or "context.getScope()" or something of that kind. – johanneslink Nov 02 '20 at 07:24

0 Answers0