metasim
metasim

Reputation: 4960

How do you unit test a JavaFX controller with JUnit

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

Upvotes: 25

Views: 22162

Answers (4)

FARS
FARS

Reputation: 457

That's all you need:

@BeforeAll
static void initJfxRuntime() {
    try {
        Platform.runLater(() -> {});
    } catch (Exception ex) {
        Platform.startup(() -> {});
    }
}

Upvotes: 1

Alex Suzuki
Alex Suzuki

Reputation: 1202

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

Upvotes: 0

Sergey Grinev
Sergey Grinev

Reputation: 34528

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

Upvotes: 22

Michael Ellis
Michael Ellis

Reputation: 273

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

Upvotes: 9

Related Questions