Here's my POC refinement to the very good answer from @Phillip Gayret, following in @Mihnea Giurgea's footsteps.
My sub-question: How to access that shared singleton resource? (Perhaps you are also wondering this...)
Sidebar: During my experimentation, I discovered that using multiple @ExtendWith(...)
seems to nest properly. Even so, I remember it not working that way at some point in my fumbling, so you should make sure your usage case(s) are workings rightly [sic].
Because you are probably in a rush, the dessert comes first: Here is the output of running "all tests within the folder":
NestedSingleton::beforeAll (setting resource)
Singleton::Start-Once
Base::beforeAll
Colors::blue - resource=Something nice to share!
Colors::gold - resource=Something nice to share!
Base::afterAll
Base::beforeAll
Numbers::one - resource=Something nice to share!
Numbers::tre - resource=Something nice to share!
Numbers::two - resource=Something nice to share!
Base::afterAll
Singleton::Finish-Once
NestedSingleton::close (clearing resource)
Of course, just running a single test class gives:
NestedSingleton::beforeAll (setting resource)
Singleton::Start-Once
Base::beforeAll
Numbers::one - resource=Something nice to share!
Numbers::tre - resource=Something nice to share!
Numbers::two - resource=Something nice to share!
Base::afterAll
Singleton::Finish-Once
NestedSingleton::close (clearing resource)
And a specific test, as can now be expected:
NestedSingleton::beforeAll (setting resource)
Singleton::Start-Once
Base::beforeAll
Colors::gold - resource=Something nice to share!
Base::afterAll
Singleton::Finish-Once
NestedSingleton::close (clearing resource)
Still with me? Then you might enjoy seeing the actual code...
======================================================
junitsingletonresource/Base.java
======================================================
package junitsingletonresource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith({Singleton.class})
public abstract class Base extends BaseNestedSingleton
{
@BeforeAll public static void beforeAll() { System.out.println("Base::beforeAll"); }
@AfterAll public static void afterAll () { System.out.println("Base::afterAll" ); }
}
======================================================
junitsingletonresource/Colors.java
======================================================
package junitsingletonresource;
import org.junit.jupiter.api.Test;
public class Colors extends Base
{
@Test public void blue() { System.out.println("Colors::blue - resource=" + getResource()); }
@Test public void gold() { System.out.println("Colors::gold - resource=" + getResource()); }
}
======================================================
junitsingletonresource/Numbers.java
======================================================
package junitsingletonresource;
import org.junit.jupiter.api.Test;
public class Numbers extends Base
{
@Test public void one() { System.out.println("Numbers::one - resource=" + getResource()); }
@Test public void two() { System.out.println("Numbers::two - resource=" + getResource()); }
@Test public void tre() { System.out.println("Numbers::tre - resource=" + getResource()); }
}
======================================================
junitsingletonresource/BaseNestedSingleton.java
======================================================
package junitsingletonresource;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
/**
* My riff on Phillip Gayret's solution from: https://stackoverflow.com/a/51556718/5957643
*/
@ExtendWith({BaseNestedSingleton.NestedSingleton.class})
public abstract class BaseNestedSingleton
{
protected String getResource() { return NestedSingleton.getResource(); }
static /*pkg*/ class NestedSingleton implements BeforeAllCallback, ExtensionContext.Store.CloseableResource
{
private static boolean initialized = false;
private static String resource = "Tests should never see this value (e.g. could be null)";
private static String getResource() { return resource; }
@Override
public void beforeAll(ExtensionContext context)
{
if (!initialized) {
initialized = true;
// The following line registers a callback hook when the root test context is shut down
context.getRoot().getStore(GLOBAL).put(this.getClass().getCanonicalName(), this);
// Your "before all tests" startup logic goes here, e.g. making connections,
// loading in-memory DB, waiting for external resources to "warm up", etc.
System.out.println("NestedSingleton::beforeAll (setting resource)");
resource = "Something nice to share!";
}
}
@Override
public void close() {
if (!initialized) { throw new RuntimeException("Oops - this should never happen"); }
// Cleanup the resource if needed, e.g. flush files, gracefully end connections, bury any corpses, etc.
System.out.println("NestedSingleton::close (clearing resource)");
resource = null;
}
}
}
======================================================
junitsingletonresource/Singleton.java
======================================================
package junitsingletonresource;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
/**
* This is pretty much what Phillip Gayret provided, but with some printing for traceability
*/
public class Singleton implements BeforeAllCallback, ExtensionContext.Store.CloseableResource
{
private static boolean started = false;
@Override
public void beforeAll(ExtensionContext context)
{
if (!started) {
started = true;
System.out.println("Singleton::Start-Once");
context.getRoot().getStore(GLOBAL).put("any unique name", this);
}
}
@Override
public void close() { System.out.println("Singleton::Finish-Once"); }
}