How do you unit test a JavaFX controller with JUnit
Asked Answered
S

4

25

What's the proper way of initializing the JavaFX runtime so you can unit test (with JUnit) controllers that make use of the concurrency facilities and Platform.runLater(Runnable)?

Calling Application.launch(...) from the @BeforeClass method results in a dead lock. If Application.launch(...) is not called then the following error is thrown:


java.lang.IllegalStateException: Toolkit not initialized
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:121)
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:116)
    at javafx.application.Platform.runLater(Platform.java:52)
    at javafx.concurrent.Task.runLater(Task.java:1042)
    at javafx.concurrent.Task.updateMessage(Task.java:987)
    at com.xyz.AudioSegmentExtractor.call(AudioSegmentExtractor.java:64)
    at com.xyz.CompletionControllerTest.setUp(CompletionControllerTest.java:69)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

Followup: this is the motif I've been using based on recommendation by @SergeyGrinev.

... // Inside test class

public static class AsNonApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        // noop
    }
}

@BeforeClass
public static void initJFX() {
    Thread t = new Thread("JavaFX Init Thread") {
        public void run() {
            Application.launch(AsNonApp.class, new String[0]);
        }
    };
    t.setDaemon(true);
    t.start();
}
... // controller tests follow...
Son answered 8/7, 2012 at 18:33 Comment(0)
H
22

Calling launch() from @BeforeClass is a correct approach. Just note that launch() doesn't return control to calling code. So you have to wrap it into new Thread(...).start().

A 7 years later update:

Use TestFX! It will take care of launching in a proper way. E.g. you can extend your test from a TestFX's ApplicaionTest class and just use the same code:

public class MyTest extends ApplicationTest {

@Override
public void start (Stage stage) throws Exception {
    FXMLLoader loader = new FXMLLoader(
            getClass().getResource("mypage.fxml"));
    stage.setScene(scene = new Scene(loader.load(), 300, 300));
    stage.show();
}

and write tests like that:

@Test
public void testBlueHasOnlyOneEntry() {
    clickOn("#tfSearch").write("blue");
    verifyThat("#labelCount", hasText("1"));
}
Hinson answered 8/7, 2012 at 21:28 Comment(0)
E
9

I found this to work,... but only after adding a Thread.sleep(500) after starting the JavaFX application thread. Presumably it takes some time to get the FX environment up and ready (about 200ms on my MacBook Pro retina)

@BeforeClass
public static void setUpClass() throws InterruptedException {
    // Initialise Java FX

    System.out.printf("About to launch FX App\n");
    Thread t = new Thread("JavaFX Init Thread") {
        public void run() {
            Application.launch(AsNonApp.class, new String[0]);
        }
    };
    t.setDaemon(true);
    t.start();
    System.out.printf("FX App thread started\n");
    Thread.sleep(500);
}
Euridice answered 19/11, 2014 at 19:58 Comment(1)
[Application.launch](It must not be called more than once or an exception will be thrown.) documentation states: "It must not be called more than once or an exception will be thrown.", just something to be aware if if you try to run the setUpClass() method in this answer more than once.Moustache
B
1

That's all you need:

@BeforeAll
static void initJfxRuntime() {
    try {
        Platform.runLater(() -> {});
    } catch (Exception ex) {
        Platform.startup(() -> {});
    }
}
Blubberhead answered 17/8, 2023 at 21:57 Comment(0)
U
0

This works for me as well for testing code that uses JavaFX concurrency constructs such as javafx.concurrent.Task. Compared to the other solutions it does not need explicit Thread management or supplying a dummy Application.

@BeforeAll
public static void setUpJavaFXRuntime() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(1);
    Platform.startup(() -> {
        latch.countDown();
    });
    latch.await(5, TimeUnit.SECONDS);
}

@AfterAll
public static void tearDownJavaFXRuntime() throws InterruptedException {
    Platform.exit();
}
Unbind answered 26/5, 2021 at 10:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.