您如何模拟 JavaFX 工具包初始化?
How do you mock a JavaFX toolkit initialization?
[序言:抱歉,这里有很多代码,其中一些可能与这个问题无关,而一些理解问题所必需的代码可能会丢失;请发表评论,我会相应地编辑问题。]
环境:Ubuntu14.10 x86_64;甲骨文 JDK 1.8u25。单元测试库为TestNG,版本6.8.13; Mockito 是版本 1.10.17.
在我的 GUI 应用程序中,JavaFX 所谓的 "controller" 是相当被动的,因为这个 "controller"(我称之为 "display")真正做的唯一事情是发送事件。
现在,当接收到需要更新 GUI 的事件时,它是另一个 class,我称之为视图,它负责更新 GUI。简而言之:
显示 -> 演示者 -> 查看 -> 显示
我对其中两个进行了单元测试:
- 显示 -> 演示者;
- 演示者 -> 查看。
所以,我在这方面已经很熟悉了(优点是我可以更改显示,这就是我这样做的原因)。
但现在我尝试测试 "view -> display" 部分;我是 SOL。
作为示例,这里是视图 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);
}
}
匹配显示class是这样的:
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"));
}
}
}
这是我的测试文件:
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);
}
}
我原以为它会起作用...但它不起作用。然而 "far apart" 我试图避开平台代码,即使上面的测试 class 也失败了,出现了这个异常:
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
所以,简而言之,如何防止上述异常发生?我原以为模拟掉小部件就足够了,但显然不是:/ 看起来我需要模拟整个 "platform context" (因为没有更好的词来形容它)但我不知道如何.
好的,首先要注意的是:我一生中从未使用过 Mockito。但我很好奇,所以我花了几个小时来弄清楚这一点,我想还有很多需要改进的地方。
因此,为了使它正常工作,我们需要:
- The aforementioned (by @jewelsea) JUnit Threading Rule.
- 自定义
MockMaker
实现,包装默认 CglibMockMaker
。
- 将所有东西连接在一起。
所以 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> 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);
}
}
3号只是按照说明书:
- 复制我们 MockMaker 的完全限定 class 名称,例如。
org.awesome.mockito.JavaFXMockMaker
.
- 创建一个文件"mockito-extensions/org.mockito.plugins.MockMaker"。该文件的内容恰好是带有限定名称的一行。
祝您测试愉快,感谢 Andy Till 的线程规则。
警告:这种实现对 MockMaker
to use the CglibMockMaker
which might not be be what you want in every case (see the JavaDocs).
进行了硬编码
[序言:抱歉,这里有很多代码,其中一些可能与这个问题无关,而一些理解问题所必需的代码可能会丢失;请发表评论,我会相应地编辑问题。]
环境:Ubuntu14.10 x86_64;甲骨文 JDK 1.8u25。单元测试库为TestNG,版本6.8.13; Mockito 是版本 1.10.17.
在我的 GUI 应用程序中,JavaFX 所谓的 "controller" 是相当被动的,因为这个 "controller"(我称之为 "display")真正做的唯一事情是发送事件。
现在,当接收到需要更新 GUI 的事件时,它是另一个 class,我称之为视图,它负责更新 GUI。简而言之:
显示 -> 演示者 -> 查看 -> 显示
我对其中两个进行了单元测试:
- 显示 -> 演示者;
- 演示者 -> 查看。
所以,我在这方面已经很熟悉了(优点是我可以更改显示,这就是我这样做的原因)。
但现在我尝试测试 "view -> display" 部分;我是 SOL。
作为示例,这里是视图 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);
}
}
匹配显示class是这样的:
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"));
}
}
}
这是我的测试文件:
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);
}
}
我原以为它会起作用...但它不起作用。然而 "far apart" 我试图避开平台代码,即使上面的测试 class 也失败了,出现了这个异常:
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
所以,简而言之,如何防止上述异常发生?我原以为模拟掉小部件就足够了,但显然不是:/ 看起来我需要模拟整个 "platform context" (因为没有更好的词来形容它)但我不知道如何.
好的,首先要注意的是:我一生中从未使用过 Mockito。但我很好奇,所以我花了几个小时来弄清楚这一点,我想还有很多需要改进的地方。
因此,为了使它正常工作,我们需要:
- The aforementioned (by @jewelsea) JUnit Threading Rule.
- 自定义
MockMaker
实现,包装默认CglibMockMaker
。 - 将所有东西连接在一起。
所以 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> 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);
}
}
3号只是按照说明书:
- 复制我们 MockMaker 的完全限定 class 名称,例如。
org.awesome.mockito.JavaFXMockMaker
. - 创建一个文件"mockito-extensions/org.mockito.plugins.MockMaker"。该文件的内容恰好是带有限定名称的一行。
祝您测试愉快,感谢 Andy Till 的线程规则。
警告:这种实现对 MockMaker
to use the CglibMockMaker
which might not be be what you want in every case (see the JavaDocs).