2015-01-30 19 views
16

[preambuła: przepraszam, jest tu dużo kodu, a część z nich może nie być odpowiednia dla tego pytania, podczas gdy może brakować kodu niezbędnego do zrozumienia problemu; proszę o komentarz, a ja odpowiednio zmienię pytanie.]W jaki sposób kpisz z inicjowania zestawu narzędzi JavaFX?

Środowisko: Ubuntu 14.10 x86_64; Oracle JDK 1.8u25. Biblioteka testów jednostkowych to TestNG, wersja 6.8.13; Mockito to wersja 1.10.17.

W mojej aplikacji GUI to, co JavaFX nazywa "kontrolerem", jest dość pasywne, w tym znaczeniu, że jedyną rzeczą, którą naprawdę robi ten "kontroler" (który nazywam "wyświetlaczem") jest wysyłanie zdarzeń.

Teraz, po odebraniu zdarzenia, które wymaga aktualizacji GUI, jest to kolejna klasa, którą nazywam widokiem, która jest odpowiedzialna za aktualizację GUI. W skrócie:

wyświetlacza -> prezenter -> Widok -> Wyświetlacz

mam testy jednostkowe dla dwóch z nich:

  • wyświetlacza -> prezenter;
  • prezenter -> widok.

Tak, jestem prawie pokryta tym frontem (z tą zaletą, że mogę zmienić wyświetlacz, dlatego robię to w ten sposób).

Ale teraz próbuję i przetestować część "widok -> wyświetlacz"; i jestem SOL.

Jako ilustracja, tutaj jest klasa widok:

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

Klasa dopasowanie wyświetlacza to:

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

i tu jest mój plik testowy:

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 spodziewałem się, że zadziała ... Z tym, że tak nie jest. Jednak „daleko od siebie” Staram się kierować z dala od platformy kod, nawet klasa Test powyżej nie z tego wyjątku:

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 

Tak, w skrócie, w jaki sposób mogę zapobiec wyjątek powyżej dzieje? Pomyślałbym, że kpiny z widżetów byłyby wystarczające, ale najwyraźniej nie:/Wygląda na to, że muszę kpić z całego "kontekstu platformy" (z braku lepszego określenia), ale nie mam pojęcia jak.

+1

Nie mam doświadczenia z konkretnymi narzędziami w twoim pakiecie narzędzi, więc nie mogę ci tam doradzić, ale możesz być w stanie zastosować coś ze strategii, która była używana do [inicjalizacji zestawu narzędzi JavaFX do testowania jednostkowego w środowisko JUnit] (https://gist.github.com/andytill/3835914). – jewelsea

+0

@jewelsea ciekawe! Spróbuję przystosować to do TestNG – fge

+0

Adaptacja @jewelsea nie powiodła się, niestety:/Nie mogę jej uruchomić ... – fge

Odpowiedz

10

Ok, pierwsze rzeczy pierwsze: nigdy nie użyłem Mockito raz w życiu. Ale byłem ciekawy, więc spędziłem kilka godzin, aby to zrozumieć i myślę, że jest wiele do poprawienia.

Tak, aby uzyskać tej pracy, musimy:

  1. The aforementioned (by @jewelsea) JUnit Threading Rule.
  2. niestandardową implementację MockMaker, owijając domyślnego CglibMockMaker.
  3. Połącz elementy razem.

więc 1 + 2 to:

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

} 

Numer 3 jest tylko po podręczniku:

  1. Copy pełną nazwą klasy naszego MockMaker, np. org.awesome.mockito.JavaFXMockMaker.
  2. Utwórz plik "mockito-extensions/org.mockito.plugins.MockMaker". Treść tego pliku to dokładnie jedna linia z kwalifikowaną nazwą.

Happy testowanie & kudos do Andy Till za jego regułę wątków.


Ostrzeżenie: Ten rodzaj realizacja twardych koduje MockMaker używać CglibMockMaker który nie może być to, co chcesz w każdym przypadku (patrz JavaDocs).

+0

Jak dyskutowano w pokoju: prosty test zadziałał dobrze; imponująca ilość badań! Spróbuję jeszcze kilku testów, ale jeśli o mnie chodzi, to zasłużony +2525 dla ciebie – fge

+0

Jeszcze 5 godzin, zanim będę mógł nagradzać nagrodę, ale to na pewno twoja! Gorąco piszę kod testowy dla wszystkich moich 'JavafxView's i wszystko działa! – fge

+0

Cieszę się, że to działa :-) – eckig

Powiązane problemy