java unit-testing javafx testng mockito

¿Cómo te burlas de la inicialización de un kit de herramientas JavaFX?



unit-testing testng (1)

Ok, lo primero es lo primero: nunca usé Mockito una vez en la vida. Pero tenía curiosidad, así que dediqué varias horas a resolver esto y creo que hay mucho que mejorar.

Para que esto funcione, necesitamos:

  1. La regla de subprocesos JUnit mencionada anteriormente (por @jewelsea).
  2. Una implementación personalizada de MockMaker , envolviendo el CglibMockMaker predeterminado.
  3. Conecta las cosas juntas.

Entonces 1 + 2 es esto:

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

El número 3 está simplemente siguiendo el manual:

  1. Copie el nombre de clase completo de nuestro MockMaker, por ejemplo. org.awesome.mockito.JavaFXMockMaker .
  2. Cree un archivo "mockito-extensions / org.mockito.plugins.MockMaker". El contenido de este archivo es exactamente una línea con el nombre calificado.

Felices pruebas y felicitaciones a Andy Till por su regla de subprocesos.

Advertencia: este tipo de implementación de códigos duros de MockMaker para usar CglibMockMaker puede no ser lo que se desea en todos los casos (consulte los MockMaker ).

[preámbulo: disculpas, hay una gran cantidad de código aquí, y parte de él puede no ser relevante para esta pregunta, mientras que puede faltar el código necesario para entender el problema; por favor comente, y editaré la pregunta en consecuencia.]

Ambiente: Ubuntu 14.10 x86_64; Oracle JDK 1.8u25. La biblioteca de pruebas de unidades es TestNG, versión 6.8.13; Mockito es la versión 1.10.17.

En mi aplicación GUI, lo que JavaFX llama "controlador" es bastante pasivo, en el sentido de que lo único que este "controlador" (al que llamo "pantalla") realmente hace es enviar eventos.

Ahora, cuando se recibe un evento que requiere una actualización de GUI, es otra clase, a la que llamo vista, que es responsable de actualizar la GUI. En breve:

pantalla -> presentador -> vista -> pantalla

Tengo pruebas de unidad para dos de estos:

  • pantalla -> presentador;
  • presentador -> vista.

Por lo tanto, estoy bastante cubierto en este frente (con la ventaja de que puedo cambiar la pantalla, razón por la cual lo hago de esa manera).

Pero ahora trato de probar la parte "ver -> mostrar"; y yo soy SOL.

Como ilustración, aquí está la clase de vista:

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

La clase de visualización coincidente es esta:

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

Y aquí está mi archivo de prueba:

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

Esperaba que funcionara ... Excepto que no funciona. Sin embargo, "lejos" trato de alejarme del código de la plataforma, incluso la clase de prueba anterior falla con esta excepción:

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

Entonces, en resumen, ¿cómo evito que ocurra la excepción anterior? Habría pensado que burlarse de los widgets habría sido suficiente, pero aparentemente no: / Parece que necesito burlarme de todo el "contexto de la plataforma" (por falta de una palabra mejor para eso) pero no tengo idea de cómo.