在 JUnit 5中,如何在所有测试之前运行代码

@BeforeAll注释标记了一个在 同学们中的所有测试之前运行的方法。

Http://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

但是有没有办法在 所有测试之前在所有类中运行一些代码呢?

我希望确保测试使用一组特定的数据库连接,并且这些连接的全局一次性设置必须发生 之前运行 任何测试。

69102 次浏览

I am not aware of a mean to do that.

I would simply make sure that all code for @BeforeAll calls a certain singleton to make that init work (probably in a lazy way to avoid repetition).

Probably not convenient ... the only other option I see: I assume your tests run within a specific JVM job. You could hook an agent into that JVM run, that does that init work for you.

Beyond that: both suggestions sounds somehow like a hack to me. The real answer in my eyes: step back, and carefully examine your environment on its dependencies. And then find a way to prepare your environment in a way that your tests come up and the "right thing" happens automatically. In other words: consider looking into the architecture that bought you this problem.

This is now possible in JUnit5 by creating a custom Extension, from which you can register a shutdown hook on the root test-context.

Your extension would look like this;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;


public class YourExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {


private static boolean started = false;


@Override
public void beforeAll(ExtensionContext context) {
if (!started) {
started = true;
// Your "before all tests" startup logic goes here
// The following line registers a callback hook when the root test context is shut down
context.getRoot().getStore(GLOBAL).put("any unique name", this);
}
}


@Override
public void close() {
// Your "after all tests" logic goes here
}
}

Then, any tests classes where you need this executed at least once, can be annotated with:

@ExtendWith({YourExtension.class})

When you use this extension on multiple classes, the startup and shutdown logic will only be invoked once.

Above advises do not work for me, so I solved this problem like this:

Add to your Base abstract class (I mean abstract class where you initialize your driver in setUpDriver() method) this part of code:

private static boolean started = false;
static{
if (!started) {
started = true;
try {
setUpDriver();  //method where you initialize your driver
} catch (MalformedURLException e) {
}
}
}

And now, if your test classes will extends from Base abstract class -> setUpDriver() method will be executed before first @Test only ONE time per project run.

Extending on suggestion from @Philipp, here's a more complete code snippet:

import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;


public abstract class BaseSetupExtension
implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {


@Override
public void beforeAll(ExtensionContext context) throws Exception {
// We need to use a unique key here, across all usages of this particular extension.
String uniqueKey = this.getClass().getName();
Object value = context.getRoot().getStore(GLOBAL).get(uniqueKey);
if (value == null) {
// First test container invocation.
context.getRoot().getStore(GLOBAL).put(uniqueKey, this);
setup();
}
}


// Callback that is invoked <em>exactly once</em>
// before the start of <em>all</em> test containers.
abstract void setup();


// Callback that is invoked <em>exactly once</em>
// after the end of <em>all</em> test containers.
// Inherited from {@code CloseableResource}
public abstract void close() throws Throwable;
}

How to use:

public class DemoSetupExtension extends BaseSetupExtension {
@Override
void setup() {}


@Override
public void close() throws Throwable {}
}


@ExtendWith(DemoSetupExtension.class)
public class TestOne {
@BeforeAll
public void beforeAllTestOne { ... }


@Test
public void testOne { ... }
}


@ExtendWith(DemoSetupExtension.class)
public class TestTwo {
@BeforeAll
public void beforeAllTestTwo { ... }


@Test
public void testTwo { ... }
}

Test execution order will be:

  DemoSetupExtension.setup (*)
TestOne.beforeAllTestOne
TestOne.testOne
TestOne.afterAllTestOne
TestTwo.beforeAllTestTwo
TestTwo.testTwo
TestTwo.afterAllTestTwo
DemoSetupExtension.close (*)

...this will be true regardless if you choose to run a single @Test (e.g. TestOne.testOne), or an entire test class (TestOne), or multiple / all tests.

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"); }
}


The already provided answer from @Philipp Gayret has some problems when testing JUnit in parallel (i.e. junit.jupiter.execution.parallel.enabled = true).

Therefore I adapted the solution to:

import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;


public class BeforeAllTestsExtension extends BasicTestClass
implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {


/** Gate keeper to prevent multiple Threads within the same routine */
private static final Lock LOCK = new ReentrantLock();
/** volatile boolean to tell other threads, when unblocked, whether they should try attempt start-up.  Alternatively, could use AtomicBoolean. */
private static volatile boolean started = false;
    

@Override
public void beforeAll(final ExtensionContext context) throws Exception {
// lock the access so only one Thread has access to it
LOCK.lock();
try {
if (!started) {
started = true;
// Your "before all tests" startup logic goes here
// The following line registers a callback hook when the root test context is
// shut down
context.getRoot().getStore(GLOBAL).put("any unique name", this);


// do your work - which might take some time -
// or just uses more time than the simple check of a boolean
}
} finally {
// free the access
LOCK.unlock();
}
}


@Override
public void close() {
// Your "after all tests" logic goes here
}
}

As mentioned below JUnit5 provides an automatic Extension Registration. To do so add a in src/test/resources/ a directory called /META-INF/services and add a file named org.junit.jupiter.api.extension.Extension. Add into this file the fully classified name of your class, e.g.

at.myPackage.BeforeAllTestsExtension

Next enable in the same Junit config file

junit.jupiter.extensions.autodetection.enabled=true

With this the extension is attached automatically to all of your tests.