4

Environment:

  • Oracle JDK 1.8u31;
  • Intellij IDEA 14.0.3;
  • Mockito 1.10.17;
  • TestNG 6.8.13.

First, a quick goal about the architecture: what JavaFX calls a "controller" I call a display, and I have a view class which controls the elements of the display.

All such views inherit a common base class:

public abstract class JavafxView<P, D extends JavafxDisplay<P>>
{
    protected final Node node;
    protected final D display;

    protected JavafxView(final String fxmlLocation)
        throws IOException
    {
        final URL url = JavafxView.class.getResource(fxmlLocation);
        if (url == null)
            throw new IOException(fxmlLocation + ": resource not found");
        final FXMLLoader loader = new FXMLLoader(url);
        node = loader.load();
        display = loader.getController();
    }

    @SuppressWarnings("unchecked")
    @NonFinalForTesting
    public <T extends Node> T getNode()
    {
        return (T) node;
    }

    @NonFinalForTesting
    public D getDisplay()
    {
        return display;
    }
}

At first I had a problem when I started testing since I would get that "toolkit not initialized" each and every time. However, after asking the question I was provided with a solution which worked pretty well; I could now write tests to test the behavior without a problem...

Except that I now stumble upon a test in which I get this error again:

java.lang.ExceptionInInitializerError
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
    at java.lang.Class.newInstance(Class.java:438)
    at sun.reflect.misc.ReflectUtil.newInstance(ReflectUtil.java:51)
    at javafx.fxml.FXMLLoader$InstanceDeclarationElement.constructValue(FXMLLoader.java:1001)
    at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:742)
    at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2701)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2521)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2435)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2403)
    at com.github.fge.grappa.debugger.javafx.JavafxView.<init>(JavafxView.java:24)
    at com.github.fge.grappa.debugger.csvtrace.tabs.matches.JavafxMatchesTabView.<init>(JavafxMatchesTabView.java:24)
    at com.github.fge.grappa.debugger.csvtrace.tabs.matches.JavafxMatchesTabViewTest.init(JavafxMatchesTabViewTest.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:84)
    at org.testng.internal.Invoker.invokeConfigurationMethod(Invoker.java:564)
    at org.testng.internal.Invoker.invokeConfigurations(Invoker.java:213)
    at org.testng.internal.Invoker.invokeMethod(Invoker.java:653)
    at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901)
    at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231)
    at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127)
    at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111)
    at org.testng.TestRunner.privateRun(TestRunner.java:767)
    at org.testng.TestRunner.run(TestRunner.java:617)
    at org.testng.SuiteRunner.runTest(SuiteRunner.java:348)
    at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:343)
    at org.testng.SuiteRunner.privateRun(SuiteRunner.java:305)
    at org.testng.SuiteRunner.run(SuiteRunner.java:254)
    at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
    at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
    at org.testng.TestNG.runSuitesSequentially(TestNG.java:1224)
    at org.testng.TestNG.runSuitesLocally(TestNG.java:1149)
    at org.testng.TestNG.run(TestNG.java:1057)
    at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:111)
    at org.testng.remote.RemoteTestNG.initAndRun(RemoteTestNG.java:204)
    at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:175)
    at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:125)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.IllegalStateException: Toolkit not initialized
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:270)
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:265)
    at com.sun.javafx.application.PlatformImpl.setPlatformUserAgentStylesheet(PlatformImpl.java:540)
    at com.sun.javafx.application.PlatformImpl.setDefaultPlatformUserAgentStylesheet(PlatformImpl.java:502)
    at javafx.scene.control.Control.<clinit>(Control.java:87)
    ... 47 more

JavafxView.java:24 is this line:

node = loader.load();

However, that is not the most strange.

I have two other test classes testing two other JavafxView which work without a problem when I run the test classes individually; only this new test doesn't...

... But if I run the whole test suite instead of only that test then the test suceeds!

Here is the full source code of the test:

public class JavafxMatchesTabViewTest
{
    private JavafxMatchesTabView view;
    private MatchesTabDisplay display;

    @BeforeMethod
    public void init()
        throws IOException
    {
        view = new JavafxMatchesTabView();
        display = view.getDisplay();
    }

    @Test
    public void showMatchesTest()
    {
        final List<MatchStatistics> oldStats = Arrays.asList(
            mock(MatchStatistics.class),
            mock(MatchStatistics.class)
        );

        final List<MatchStatistics> newStats = Arrays.asList(
            mock(MatchStatistics.class),
            mock(MatchStatistics.class)
        );

        final TableView<MatchStatistics> tableView = spy(new TableView<>());
        display.matchesTable = tableView;

        final ObservableList<MatchStatistics> tableData = tableView.getItems();
        final ObservableList<TableColumn<MatchStatistics, ?>> sortOrder
            = tableView.getSortOrder();

        tableData.setAll(oldStats);
        sortOrder.clear();

        view.showMatches(newStats);

        assertThat(tableData).containsExactlyElementsOf(newStats);
        assertThat(sortOrder).containsExactly(display.nrCalls);
        verify(tableView).sort();
    }
}

So, uh, how do I fix that? At a first glance it would seem that the solution could be that I kind of "port" the custom MockMaker to a TestNG's @BeforeClass... Except that I don't see how I can really do that :/

Community
  • 1
  • 1
fge
  • 119,121
  • 33
  • 254
  • 329

2 Answers2

6

Well, uh, it was more simple than I thought, and as is often the case I find the solution only once I've asked the question...

Anyway, the solution is to create an abstract base class which initializes the toolkit for you and it is as "easy" as this:

@Test
public abstract class JavafxViewTest
{
    @BeforeClass
    public static void initToolkit()
        throws InterruptedException
    {
        final CountDownLatch latch = new CountDownLatch(1);
        SwingUtilities.invokeLater(() -> {
            new JFXPanel(); // initializes JavaFX environment
            latch.countDown();
        });

        // That's a pretty reasonable delay... Right?
        if (!latch.await(5L, TimeUnit.SECONDS))
            throw new ExceptionInInitializerError();
    }
}
spiegelm
  • 65
  • 7
fge
  • 119,121
  • 33
  • 254
  • 329
  • 1
    This worked for me, however I really would like to know more detail on why SwingUtilities is used ?? Also I had to add static to initToolkit, before it could work – serup Jul 11 '16 at 08:32
4

Well, I guess we have to apply the same recipe to @BeforeClass and/or @BeforeMethod.. like so (untested):

private boolean jfxIsSetup;

private void doOnJavaFXThread(Runnable pRun) throws RuntimeException {
    if (!jfxIsSetup) {
        setupJavaFX();
        jfxIsSetup = true;
    }
    final CountDownLatch countDownLatch = new CountDownLatch(1);
    Platform.runLater(() -> {
        pRun.run();
        countDownLatch.countDown();
    });

    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

protected void setupJavaFX() throws RuntimeException {
    final CountDownLatch latch = new CountDownLatch(1);
    SwingUtilities.invokeLater(() -> {
        new JFXPanel(); // initializes JavaFX environment
        latch.countDown();
    });

    try {
        latch.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}


@BeforeMethod
public void init() throws IOException
{
    AtomicReference<JavafxMatchesTabView> tabView = new AtomicReference<>();
    AtomicReference<MatchesTabDisplay> tabDisplay = new AtomicReference<>();
    doOnJavaFXThread(()->{
        tabView.set(new JavafxMatchesTabView());
        tabDisplay.set(view.getDisplay());
    });
    view = tabView.get();
    display = tabDisplay.get();
}

If this works, you are probably better of if you move this code to an abstract base test class ;-)

eckig
  • 10,964
  • 4
  • 38
  • 52
  • In fact I did more simple than that... But your solution seems more robust – fge Feb 13 '15 at 14:16
  • I really dont know, I would have to test and verify this. The problem with your solution might (and I have to stress the might) arise other issues, as the `@BeforeXYZ` methods are still not on the FX application Thread. – eckig Feb 13 '15 at 14:17
  • Hmwell, I use an FXMLLoader, isn't that somewhat guaranteed to run on this thread? – fge Feb 13 '15 at 14:18
  • As far as I know the FXMLLoader does not make any promises regarding this. I know this: Up to a certain point you can construct JavaFX GUIs in a different thread, but certain components (like the ToolTip) will enforce the usage of the application Thread and throw an Exception. – eckig Feb 13 '15 at 14:32
  • OK, so basically I need to ensure that all the potential spies of anything JavaFX are created on this thread, huh? And that right now this code only works by pure luck? Grr – fge Feb 13 '15 at 15:08
  • Well, my initial code was not that "bad"; I simply ensured to build anything JavaFX related on the UI thread like you recommended. Note that `@BeforeClass` is guaranteed to complete its execution before any test method runs, therefore there is no need to check for the UI toolkit initialization everytime – fge Feb 13 '15 at 16:13
  • I fear so, yes. And I would never dare call anything of your writings "bad". – eckig Feb 13 '15 at 16:14
  • Well, meh, I _do_ write bad code when I don't know what I am doing, and with JavaFX this is pretty much the case :p I have many difficulties finding the frontier between what should run on the UI thread or nor as far as tests are concerned; is this only for widget initialization? – fge Feb 13 '15 at 17:09