2015-01-30 12 views
16

[preambolo: ci scusiamo, qui c'è un sacco di codice, e alcuni potrebbero non essere pertinenti a questa domanda, mentre potrebbe mancare del codice necessario per capire il problema; per favore commenta e modifico la domanda di conseguenza.]Come si prende in giro l'inizializzazione del toolkit JavaFX?

Ambiente: Ubuntu 14.10 x86_64; Oracle JDK 1.8u25. La libreria di test delle unità è TestNG, versione 6.8.13; Mockito è la versione 1.10.17.

Nella mia applicazione GUI, quello che JavaFX chiama un "controller" è piuttosto passivo, nel senso che l'unica cosa che questo "controller" (che io chiamo "display") fa davvero è inviare eventi.

Ora, quando viene ricevuto un evento che richiede un aggiornamento della GUI, è un'altra classe, che io chiamo una vista, che è responsabile dell'aggiornamento della GUI. In breve:

visualizzazione -> presenter -> vista -> visualizzazione

ho test di unità per due di questi:

  • visualizzazione -> presentatore;
  • presentatore
  • -> visualizzazione.

Quindi, sono praticamente coperto da questo fronte (con il vantaggio di poter cambiare il display, ecco perché lo sto facendo in questo modo).

Ma ora provo a testare la parte "visualizza -> visualizza"; e io sono SOL.

A titolo di esempio, ecco la classe di visualizzazione:

@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 classe di visualizzazione di corrispondenza è questo:

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

Ed ecco il mio file di test:

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 mi aspettavo che funzionasse ... Tranne che no. Tuttavia "lontani" Cerco di stare lontani dal codice della piattaforma, anche la classe di test precedente non riesce con questa eccezione:

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 

Così, in breve, Come posso evitare l'eccezione di cui sopra accada? Avrei pensato che prendere in giro i widget sarebbe stato sufficiente, ma apparentemente no:/Sembra che abbia bisogno di prendere in giro l'intero "contesto di piattaforma" (per mancanza di una parola migliore) ma non ho idea di come.

+1

Non ho esperienza con gli strumenti specifici nella tua toolchain, quindi non posso consigliarti lì, ma potresti essere in grado di adottare qualcosa da una strategia che è stata usata per [inizializzare il toolkit JavaFX per i test unitari in un ambiente JUnit] (https://gist.github.com/andytill/3835914). – jewelsea

+0

@jewelsea interessante! Cercherò di adattare questo a TestNG – fge

+0

@jewelsea adattamento fallito, sfortunatamente:/Non riesco a farlo funzionare ... – fge

risposta

10

Ok, prima le cose: non ho mai usato Mockito una volta nella vita. Ma ero curioso, quindi ho passato diverse ore a capirlo e immagino ci sia molto da migliorare.

Quindi, per ottenere questo lavoro, abbiamo bisogno di:

  1. The aforementioned (by @jewelsea) JUnit Threading Rule.
  2. Un'implementazione personalizzata MockMaker, avvolgendo il default CglibMockMaker.
  3. Collegare le cose insieme.

Quindi 1 + 2 è questa:

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

} 

numero 3 è solo seguendo il manuale:

  1. Copiare il nome completo della classe del nostro MockMaker, ad es. org.awesome.mockito.JavaFXMockMaker.
  2. Creare un file "mockito-extensions/org.mockito.plugins.MockMaker". Il contenuto di questo file è esattamente una riga con il nome qualificato.

test Felice & complimenti a Andy Till per la sua regola di filettatura.


Attenzione: Questo tipo implementazione di hard-codes il MockMaker di utilizzare il CglibMockMaker che potrebbero non essere essere ciò che si desidera in ogni caso (vedi la JavaDocs).

+0

Come discusso nella stanza: un semplice test ha funzionato bene; impressionante quantità di ricerche! Proverò qualche altro test, ma per quanto mi riguarda è un meritato +525 per te – fge

+0

Ancora 5 ore prima posso assegnare il premio ma è tuo, sicuramente! Sto scrivendo freneticamente codice di prova per tutti i miei 'JavafxView's e tutto funziona! – fge

+0

Felice di sentire, che funziona :-) – eckig

Problemi correlati