Hot questions for Using Mockito in javafx

Question:

[preamble: apologies, there is a lot of code here, and some of it may not be relevant to this question while some code which is necessary to understand the problem may be missing; please comment, and I will edit the question accordingly.]

Environment: Ubuntu 14.10 x86_64; Oracle JDK 1.8u25. Unit testing library is TestNG, version 6.8.13; Mockito is version 1.10.17.

In my GUI application, what JavaFX calls a "controller" is pretty passive, in the sense that the only thing that this "controller" (which I call a "display") really does is send events.

Now, when an event is received which requires a GUI update, it is another class, which I call a view, which is responsible for updating the GUI. In short:

display -> presenter -> view -> display

I have unit tests for two of these:

  • display -> presenter;
  • presenter -> view.

So, I am pretty much covered on this front (with the advantage that I can change the display, which is why I'm doing it that way).

But now I try and test the "view -> display" part; and I am SOL.

As an illustration, here is the view class:

@NonFinalForTesting
public class JavafxTreeTabView
    extends JavafxView<TreeTabPresenter, TreeTabDisplay>
    implements TreeTabView
{
    private final BackgroundTaskRunner taskRunner;

    public JavafxTreeTabView(final BackgroundTaskRunner taskRunner)
        throws IOException
    {
        super("/tabs/treeTab.fxml");
        this.taskRunner = taskRunner;
    }

    JavafxTreeTabView(final BackgroundTaskRunner taskRunner,
        final Node node, final TreeTabDisplay display)
    {
        super(node, display);
        this.taskRunner = taskRunner;
    }


    @Override
    public void loadTree(final ParseNode rootNode)
    {
        taskRunner.compute(() -> buildTree(rootNode), value -> {
            display.parseTree.setRoot(value);
            display.treeExpand.setDisable(false);
        });
    }

    @Override
    public void loadText(final InputBuffer buffer)
    {
        final String text = buffer.extract(0, buffer.length());
        display.inputText.getChildren().setAll(new Text(text));
    }

    @VisibleForTesting
    TreeItem<ParseNode> buildTree(final ParseNode root)
    {
        return buildTree(root, false);
    }

    private TreeItem<ParseNode> buildTree(final ParseNode root,
        final boolean expanded)
    {
        final TreeItem<ParseNode> ret = new TreeItem<>(root);

        addChildren(ret, root, expanded);

        return ret;
    }

    private void addChildren(final TreeItem<ParseNode> item,
        final ParseNode parent, final boolean expanded)
    {
        TreeItem<ParseNode> childItem;
        final List<TreeItem<ParseNode>> childrenItems
            = FXCollections.observableArrayList();

        for (final ParseNode node: parent.getChildren()) {
            childItem = new TreeItem<>(node);
            addChildren(childItem, node, expanded);
            childrenItems.add(childItem);
        }

        item.getChildren().setAll(childrenItems);
        item.setExpanded(expanded);
    }
}

The matching display class is this:

public class TreeTabDisplay
    extends JavafxDisplay<TreeTabPresenter>
{
    @FXML
    protected Button treeExpand;

    @FXML
    protected TreeView<ParseNode> parseTree;

    @FXML
    protected TextFlow inputText;

    @Override
    public void init()
    {
        parseTree.setCellFactory(param -> new ParseNodeCell(presenter));
    }

    @FXML
    void expandParseTreeEvent(final Event event)
    {
    }

    private static final class ParseNodeCell
        extends TreeCell<ParseNode>
    {
        private ParseNodeCell(final TreeTabPresenter presenter)
        {
            setEditable(false);
            selectedProperty().addListener(new ChangeListener<Boolean>()
            {
                @Override
                public void changed(
                    final ObservableValue<? extends Boolean> observable,
                    final Boolean oldValue, final Boolean newValue)
                {
                    if (!newValue)
                        return;
                    final ParseNode node = getItem();
                    if (node != null)
                        presenter.parseNodeShowEvent(node);
                }
            });
        }

        @Override
        protected void updateItem(final ParseNode item, final boolean empty)
        {
            super.updateItem(item, empty);
            setText(empty ? null : String.format("%s (%s)", item.getRuleName(),
                item.isSuccess() ? "SUCCESS" : "FAILURE"));
        }
    }
}

and here is my test file:

public final class JavafxTreeTabViewTest
{
    private final Node node = mock(Node.class);
    private final BackgroundTaskRunner taskRunner = new BackgroundTaskRunner(
        MoreExecutors.newDirectExecutorService(), Runnable::run
    );
    private JavafxTreeTabView view;
    private TreeTabDisplay display;

    @BeforeMethod
    public void init()
        throws IOException
    {
        display = new TreeTabDisplay();
        view = spy(new JavafxTreeTabView(taskRunner, node, display));
    }

    @Test
    public void loadTreeTest()
    {
        final ParseNode rootNode = mock(ParseNode.class);
        final TreeItem<ParseNode> item = mock(TreeItem.class);

        doReturn(item).when(view).buildTree(same(rootNode));

        display.parseTree = mock(TreeView.class);
        display.treeExpand = mock(Button.class);

        view.loadTree(rootNode);


        verify(display.parseTree).setRoot(same(item));
        verify(display.treeExpand).setDisable(false);
    }
}

I expected it to work... Except that it doesn't. However "far apart" I try to steer away from the platform code, even the test class above fails with this exception:

java.lang.ExceptionInInitializerError
    at sun.reflect.GeneratedSerializationConstructorAccessor5.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:45)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)
    at org.mockito.internal.creation.instance.ObjenesisInstantiator.newInstance(ObjenesisInstantiator.java:14)
    at org.mockito.internal.creation.cglib.ClassImposterizer.createProxy(ClassImposterizer.java:143)
    at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:58)
    at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:49)
    at org.mockito.internal.creation.cglib.CglibMockMaker.createMock(CglibMockMaker.java:24)
    at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:33)
    at org.mockito.internal.MockitoCore.mock(MockitoCore.java:59)
    at org.mockito.Mockito.mock(Mockito.java:1285)
    at org.mockito.Mockito.mock(Mockito.java:1163)
    at com.github.fge.grappa.debugger.csvtrace.tabs.JavafxTreeTabViewTest.loadTreeTest(JavafxTreeTabViewTest.java:46)
    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.invokeMethod(Invoker.java:714)
    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)
    ... 44 more

So, in short, how do I prevent the exception above from happening? I'd have thought that mocking the widgets away would have been enough, but apparently not :/ It looks like I need to mock the whole "platform context" (for lack of a better word for it) but I have no idea how.


Answer:

Ok, first things first: I never used Mockito once in a life. But I was curious, so I spent several hours to figure this out and I guess there is much to improve.

So to get this working, we need:

  1. The aforementioned (by @jewelsea) JUnit Threading Rule.
  2. A custom MockMaker implementation, wrapping the default CglibMockMaker.
  3. Wire the things together.

So 1+2 is this:

public class JavaFXMockMaker implements MockMaker {

    private final MockMaker wrapped = new CglibMockMaker();
    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);
        }
    }

    @Override
    public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
        AtomicReference<T> result = new AtomicReference<>();
        Runnable run = () -> result.set(wrapped.createMock(settings, handler));
        doOnJavaFXThread(run);
        return result.get();
    }

    @Override
    public MockHandler getHandler(Object mock) {
        AtomicReference<MockHandler> result = new AtomicReference<>();
        Runnable run = () -> result.set(wrapped.getHandler(mock));
        doOnJavaFXThread(run);
        return result.get();
    }

    @Override
    public void resetMock(Object mock, MockHandler newHandler, @SuppressWarnings("rawtypes") MockCreationSettings settings) {
        Runnable run = () -> wrapped.resetMock(mock, newHandler, settings); 
        doOnJavaFXThread(run);
    }

}

Number 3 is just following the manual:

  1. Copy the fully qualified class name of our MockMaker, eg. org.awesome.mockito.JavaFXMockMaker.
  2. Create a file "mockito-extensions/org.mockito.plugins.MockMaker". The content of this file is exactly a one line with the qualified name.

Happy testing & kudos to Andy Till for his threading rule.


Warning: This implementation kind of hard-codes the MockMaker to use the CglibMockMaker which might not be be what you want in every case (see the JavaDocs).

Question:

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 :/


Answer:

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