单元测试如何等待 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)
以下测试套件在第二次测试中失败,因为 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()
因此,经过更多试验后,错误仍然存在即使使用 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)