单元测试如何等待 JavaFX 平台关闭?

How can unit tests await the shutdown of the JavaFX Platform?

以下测试套件在第二次测试中失败,因为 JavaFX 平台在第一次测试后未正确关闭。如何等待平台终止?

理想情况下,我正在寻找与 JFX 内部使用的解决方案类似的解决方案,使用关闭挂钩(下面的失败测试用例示例)。

简化的失败测试代码:

package sample;

import javafx.application.Platform;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@DisplayName( "the Java FX Platform should" )
public class JavaFXSuite
{
    @BeforeEach
    void startJFX() throws InterruptedException
    {
        final var latch = new CountDownLatch( 1 );
        Platform.startup( latch::countDown );
        assertTrue( latch.await( 1, SECONDS ) );
    }

    @AfterEach
    void stopJFX()
    {
//      final var latch = new CountDownLatch( 1 );
//      Platform.exit( latch::countDown ); //<-- won't compile: doesn't exist
//      assertTrue( latch.await( 1, SECONDS ) );

        // Platform.exit() just sets a boolean. Next test starts before platform had been exited properly
        Platform.exit();
    }

    @Test
    @DisplayName( "create its own application thread" )
    void firstTestPassesFine()
    {
        assertFalse( Platform.isFxApplicationThread() );
    }

    @Test
    @DisplayName( "not mess with tests that don't use the platform at all" )
    void supposedlyOrthogonalSecondTestFailsOnSetUp()
    {
        assertThat( 2, is( 2 ) );
    }
}

第二次测试在设置中失败并输出:

java.lang.IllegalStateException: Platform.exit has been called

    at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:179)
    at javafx.graphics/javafx.application.Platform.startup(Platform.java:111)
    at JavaFXBla/sample.JavaFXSuite.startJFX(JavaFXSuite.java:26)
    (...etc)

通过 JavaFX 内部代码挖掘,我找到了 PlatformImpl.FinishListener。我尝试使用贷款模式来使用它。这失败了(在运行时!),因为内部 API 不是,当然也不应该通过其模块公开。

贷款模式测试夹具:

package sample;

import com.sun.javafx.application.PlatformImpl;
import javafx.application.Platform;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;

import java.util.concurrent.CountDownLatch;

import static com.sun.javafx.application.PlatformImpl.addListener;

@Log4j2
public class JavaFXSuite
{
    private static final Object lock = new Object();

    protected void runningJavaFX( @NonNull final Runnable test ) throws InterruptedException
    {
        synchronized( lock )
        {
            final var latch = addShutdownHook();

            log.info( "entered" );
            test.run();

            Platform.exit();
            latch.await();
            log.info( "exited" );
        }
    }

    private CountDownLatch addShutdownHook()
    {
        final var latch = new CountDownLatch( 1 );
        addListener( new PlatformImpl.FinishListener()
        {
            @Override
            public void idle( boolean implicitExit )
            {
                latch.countDown();
            }

            @Override
            public void exitCalled()
            {
                latch.countDown();
            }
        } );

        return latch;
    }
}

样本失败运行时输出:

java.lang.IllegalAccessError: superinterface check failed: class sample.JavaFXSuite (in module JavaFXBla) cannot access class com.sun.javafx.application.PlatformImpl$FinishListener (in module javafx.graphics) because module javafx.graphics does not export com.sun.javafx.application to module JavaFXBla

    at JavaFXBla/sample.JavaFXSuite.addShutdownHook(JavaFXSuite.java:36)
    (...etc)

注意:这是一个简化的示例。实际测试的代码(也)通过 Application.launch()

启动 JFX 运行时

因此,经过更多试验后,错误仍然存​​在即使使用 JavaFX 自己的关闭侦听机制(请参阅下面的代码)。

这使得 @James_D 他们的解释很可能是正确的:每个 JVM 实例只能启动 JavaFX 一次.

因此不可能通过在每个测试中启动私有 JavaFX 实例来正交 运行 JavaFX 单元测试。

反例:

package sample;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@DisplayName( "the Java FX Platform should" )
@Log4j2
public class JavaFXSuite
{
    // static, because that's the only way to communicate with the created application
    private static CountDownLatch startupLatch;
    private CountDownLatch shutdownLatch;

    @BeforeEach
    @DisplayName( "while started" )
    void startJFX() throws InterruptedException
    {
        startupLatch = new CountDownLatch( 1 );

        // start the JFX the 'normal' way, by letting JFX hijack the calling thread
        // and then decrementing the latch on returning
        shutdownLatch = new CountDownLatch( 1 );
        new Thread( () ->
        {
            Application.launch( Bla.class );
            shutdownLatch.countDown();
        } ).start();
        assertTrue( startupLatch.await( 1, SECONDS ) );
    }

    public static class Bla
            extends Application
    {
        public Bla(){}

        @Override
        public void start( Stage primaryStage )
        {
            startupLatch.countDown();
        }
    }

    @AfterEach
    @DisplayName( "until halted" )
    void stopJFX() throws InterruptedException
    {
        Platform.exit();
        assertTrue( shutdownLatch.await( 1, SECONDS ) );
    }

    @Test
    @DisplayName( "create its own application thread" )
    void firstTestPassesFine()
    {
        assertFalse( Platform.isFxApplicationThread() );
    }

    @Test
    @DisplayName( "not mess with tests that don't use the platform at all" )
    void supposedlyOrthogonalSecondTestFailsOnSetUp()
    {
        assertThat( 2, is( 2 ) );
    }
}

第二次测试失败消息:

Exception in thread "Thread-4" java.lang.IllegalStateException: Application launch must not be called more than once
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:175)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:156)
    at javafx.graphics/javafx.application.Application.launch(Application.java:233)
    at JavaFXBla/sample.JavaFXSuite.lambda$startJFX[=11=](JavaFXSuite.java:40)
    at java.base/java.lang.Thread.run(Thread.java:831)