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

122

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

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

但是有没有一种方式可以在所有类中的所有测试之前运行一些代码?

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


1
只是为了记录:如果您发现其中一个答案足够有帮助,请考虑在某个时候接受其中之一;-) - GhostCat
6个回答

162

现在在JUnit5中,通过创建自定义扩展(extension)即可实现此操作,您可以在该扩展(extension)上注册一个关机钩子(shutdown hook)来管理根测试上下文(root test-context)。

您的扩展(extension)应该类似于这样:

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
    }
}

然后,任何需要执行至少一次的测试类可以使用以下注释进行注释:

@ExtendWith({YourExtension.class})
当您在多个类上使用此扩展时,启动和关闭逻辑只会被调用一次。

7
如果你需要对每个测试类都执行此操作,那么你也可以使用自动扩展注册来使用ServiceLoader注册扩展。这样,你就不需要为每个单独的测试进行注释(有可能会遗漏一些), - kapex
6
在我的情况下,CloseableResource 中的 close 方法没有被调用,但是 AfterAllCallback 中的 afterAll 方法被调用了。 - Emmanuel Touzery
1
顺便说一下,我使用Spring实现了这个功能,使用DataSource bean设置测试数据,效果很好;详细信息请参见https://dev59.com/WVMI5IYBdhLWcg3wJ4Kz#62504238。没有你的帮助回答,我无法完成它,谢谢! - Steve K
我忍不住发了一个引人注目的“更进一步”的回答。如果您认为它有助于对话,请投票支持:https://dev59.com/GFgQ5IYBdhLWcg3wIASb#65450782 - Stevel
2
你应该使用getOrComputeIfAbsent方法来避免使用静态变量started,并使其线程安全。 - kan
显示剩余4条评论

28

@Philipp Gayret提供的答案在测试JUnit时存在一些问题,特别是在并行测试中(即junit.jupiter.execution.parallel.enabled = true)。

因此,我对解决方案进行了改进:

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
    }
}

如下所述,JUnit5提供了自动扩展注册功能。要使用此功能,请在src/test/resources/中添加一个名为/META-INF/services的目录,并添加一个名为org.junit.jupiter.api.extension.Extension的文件。在此文件中添加您的类的完全限定名称,例如:

at.myPackage.BeforeAllTestsExtension

接下来在同一个 JUnit 配置文件中启用

junit.jupiter.extensions.autodetection.enabled=true

使用此方法,扩展功能将自动附加到您的所有测试中。


运行完美!提示:扩展类必须是公共的,就像这个例子一样。 - RoBeaToZ
1
@RyanMoser:请纠正/更新它。谢谢。 - LeO
你应该使用@ResourceLock而不是其他方式。 - Sylvain Lecoy
代码运行得很好,但是在上面的例子中,这行代码有什么用呢?"context.getRoot().getStore(GLOBAL).put("任意唯一名称", this);" - Pramod Yadav
@PramodYadav:只需复制粘贴原始代码并进行适应。别以为我有责任“正确地”回答它;-) - LeO
显示剩余8条评论

6

延续了@Philipp的建议,下面是一个更完整的代码片段:

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

使用方法:

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 { ... }
}

测试执行顺序将是:

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

无论您选择运行单个@Test(例如TestOne.testOne),还是整个测试类(TestOne),或多个/所有测试,这都将成为真实情况。

第一次运行成功,当与自动注册扩展的信息结合使用时。 - Andrew Lazarus
它为什么只关闭一次?可能是因为:https://junit.org/junit5/docs/current/user-guide/#extensions-keeping-state - dforce

2
以上建议对我无效,所以我是这样解决这个问题的:
在你的Base抽象类中(我的意思是在你的setUpDriver()方法中初始化驱动程序的抽象类),添加以下代码部分:
"Original Answer"翻译成中文为"最初的回答"。
private static boolean started = false;
static{
    if (!started) {
        started = true;
        try {
            setUpDriver();  //method where you initialize your driver
        } catch (MalformedURLException e) {
        }
    }
}

现在,如果你的测试类继承自抽象基类Base -> setUpDriver() 方法将在第一个@Test之前仅执行一次,每个项目运行一次。


2

我不知道有什么方法可以做到这一点。

我只需要确保所有的 @BeforeAll 代码调用一个特定的单例来使初始化工作正常运行(可能以懒惰的方式避免重复)。

可能不太方便...我唯一看到的另一个选择是:我假设你的测试在特定的 JVM 作业中运行。你可以将一个代理程序agent挂钩到该 JVM 运行中,为你执行那个初始化工作。

除此之外:这两个建议听起来都有点像 hack。在我看来,真正的答案是:退后一步,仔细检查你的环境及其依赖关系。然后找到一种方法,在你的测试启动并且“正确的事情”自动发生的情况下,为你的环境做好准备。换句话说,考虑研究导致你出现这个问题的架构。


0

这是我对@Phillip Gayret的非常好的答案进行POC改进,跟随@Mihnea Giurgea的脚步。

我的子问题:如何访问共享单例资源?(也许您也在想这个问题...)

侧边栏:在我的实验中,我发现使用多个@ExtendWith(...)似乎嵌套正确。即便如此,在我摸索的某个时刻,我记得它并不是那样工作的,所以您应该确保您的用例正常工作。

因为您可能很匆忙,甜点先上:这是运行“文件夹内所有测试”的输出:

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)

当然,只运行一个测试类会返回:

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)

还有一个特定的测试,现在可以期望:

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)

还跟上我了吗?那么你可能会喜欢看到实际的代码...

======================================================
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://dev59.com/GFgQ5IYBdhLWcg3wIASb#51556718
 */
@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"); }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接