[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:
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.
Mockito is a java based mocking framework, used in conjunction with other testing frameworks such as JUnit and TestNG. It internally uses Java Reflection API and allows to create objects of a service. A mock object returns a dummy data and avoids external dependencies.
TestFx is a unit test framework, so it is designed to grab parts of your GUI implementation and test on that. That requires you to make these parts available first and test targets (buttons etc) available by tagging them with ids.
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:
MockMaker
implementation, wrapping the default CglibMockMaker
.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:
org.awesome.mockito.JavaFXMockMaker
.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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With