你如何模拟JavaFX工具包初始化?

[序言:道歉,这里有很多代码,其中一些代码可能与此问题无关,而某些代码对于理解问题可能缺失; 请评论,我会相应地编辑问题。]

环境:Ubuntu 14.10 x86_64; Oracle JDK 1.8u25。 unit testing库是TestNG,版本6.8.13; Mockito是版本1.10.17。

在我的GUI应用程序中,JavaFX称之为“控制器”的东西是非常被动的,因为这个“控制器”(我称之为“显示器”)真正做的唯一事情就是发送事件。

现在,当收到需要GUI更新的事件时,它是另一个类,我称之为视图,它负责更新GUI。 简而言之:

显示 – >演示者 – >视图 – >显示

我有两个unit testing:

  • 显示 – >主持人;
  • 主持人 – >查看。

所以,我在这方面已经被覆盖了(我可以改变显示器的优势,这就是我这样做的原因)。

但现在我尝试测试“视图 – >显示”部分; 我是SOL。

作为说明,这里是视图类:

@NonFinalForTesting public class JavafxTreeTabView extends JavafxView 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 buildTree(final ParseNode root) { return buildTree(root, false); } private TreeItem buildTree(final ParseNode root, final boolean expanded) { final TreeItem ret = new TreeItem(root); addChildren(ret, root, expanded); return ret; } private void addChildren(final TreeItem item, final ParseNode parent, final boolean expanded) { TreeItem childItem; final List<TreeItem> 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); } } 

匹配的显示类是这样的:

 public class TreeTabDisplay extends JavafxDisplay { @FXML protected Button treeExpand; @FXML protected TreeView 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 { private ParseNodeCell(final TreeTabPresenter presenter) { setEditable(false); selectedProperty().addListener(new ChangeListener() { @Override public void changed( final ObservableValue 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")); } } } 

这是我的测试文件:

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

我希望它能起作用……除非它没有。 然而,“相距甚远”我试图避开平台代码,即使上面的测试类失败也会出现此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.(Control.java:87) ... 44 more 

那么,简而言之,我如何防止上述exception发生? 我曾经认为嘲笑小部件就足够了,但显然不是:/看起来我需要嘲笑整个“平台环境”(因为缺少一个更好的词)但我不知道如何。

好的,首先要做的事情:我从未在生活中使用过Mockito。 但我很好奇,所以我花了几个小时来解决这个问题,我想还有很多需要改进的地方。

为了实现这一目标,我们需要:

  1. 上述(由@jewelsea)JUnit线程规则。
  2. 自定义MockMaker实现,包装默认的CglibMockMaker
  3. 把东西连在一起。

所以1 + 2是这样的:

 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 createMock(MockCreationSettings settings, MockHandler handler) { AtomicReference result = new AtomicReference<>(); Runnable run = () -> result.set(wrapped.createMock(settings, handler)); doOnJavaFXThread(run); return result.get(); } @Override public MockHandler getHandler(Object mock) { AtomicReference 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); } } 

3号就在手册之后:

  1. 复制我们的MockMaker的完全限定类名,例如。 org.awesome.mockito.JavaFXMockMaker
  2. 创建一个文件“mockito-extensions / org.mockito.plugins.MockMaker”。 此文件的内容恰好是具有限定名称的一行。

Andy Till对他的线程规则进行了快乐的测试和赞誉。


警告:这种实现类型的MockMaker硬编码使用CglibMockMaker ,它可能不是你想要的(参见JavaDocs )。