From 37df395bda1e47469942a9308952e1ec87ddcc8d Mon Sep 17 00:00:00 2001 From: Manuel Mauky Date: Thu, 11 Jun 2015 16:39:33 +0200 Subject: [PATCH 1/2] #263 first attempt of a testing helper for Notifications --- .../main/java/de/saxsys/mvvmfx/ViewModel.java | 2 +- .../notifications/NotificationTestHelper.java | 53 ++++++++++++++++++ .../NotificationTestHelperTest.java | 54 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java create mode 100644 mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java index cca35ce63..f465a65bc 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java @@ -45,7 +45,7 @@ public interface ViewModel { /** - * Publishes a notification to the subscribers of the notificationId. This notification will be send to the + * Publishes a notification to the subscribers of the messageName. This notification will be send to the * UI-Thread. * * @param messageName diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java new file mode 100644 index 000000000..9a9c06a8c --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java @@ -0,0 +1,53 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.util.Pair; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author manuel.mauky + */ +public class NotificationTestHelper implements NotificationObserver { + + private List> notifications = new ArrayList<>(); + + public NotificationTestHelper() { + new JFXPanel(); + } + + @Override + public void receivedNotification(String key, Object... payload) { + notifications.add(new Pair<>(key, payload)); + } + + public int numberOfCalls() { + waitForUiThread(); + return notifications.size(); + } + + public int numberOfCalls(String key) { + waitForUiThread(); + return (int) notifications.stream() + .filter(pair -> pair.getKey().equals(key)) + .count(); + } + + private void waitForUiThread() { + CompletableFuture future = new CompletableFuture<>(); + + Platform.runLater(() -> future.complete(null)); + + try { + future.get(1l, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + e.printStackTrace(); + } + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java new file mode 100644 index 000000000..63fd68db9 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java @@ -0,0 +1,54 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import de.saxsys.javafx.test.JfxRunner; +import de.saxsys.mvvmfx.ViewModel; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author manuel.mauky + */ +public class NotificationTestHelperTest { + + public class MyViewModel implements ViewModel { + } + + private MyViewModel viewModel; + + @Before + public void setUp() throws Exception { + viewModel = new MyViewModel(); + } + @Test + public void singlePublish () { + NotificationTestHelper helper = new NotificationTestHelper(); + viewModel.subscribe("test", helper); + + viewModel.publish("test"); + + assertThat(helper.numberOfCalls()).isEqualTo(1); + } + + @Test + public void multiplePublish() { + NotificationTestHelper helper = new NotificationTestHelper(); + viewModel.subscribe("test", helper); + + int n = 10; + + for(int i=0 ; i Date: Mon, 15 Jun 2015 15:19:56 +0200 Subject: [PATCH 2/2] #263 finished notification test helper --- .../main/java/de/saxsys/mvvmfx/ViewModel.java | 11 ++- .../notifications/NotificationCenter.java | 9 +- .../notifications/NotificationObserver.java | 2 +- .../notifications/NotificationTestHelper.java | 97 +++++++++++++++++-- .../NotificationTestHelperTest.java | 71 +++++++++++--- 5 files changed, 168 insertions(+), 22 deletions(-) diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java index f465a65bc..86cba9294 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java @@ -15,6 +15,7 @@ ******************************************************************************/ package de.saxsys.mvvmfx; +import de.saxsys.mvvmfx.utils.notifications.NotificationTestHelper; import javafx.application.Platform; import de.saxsys.mvvmfx.internal.viewloader.View; import de.saxsys.mvvmfx.utils.notifications.NotificationCenter; @@ -46,7 +47,15 @@ public interface ViewModel { /** * Publishes a notification to the subscribers of the messageName. This notification will be send to the - * UI-Thread. + * UI-Thread. + *

+ * + * This notification mechanism uses the {@link NotificationCenter} internally with the difference that messages send + * by this method aren't globally available. Instead they can only be received by this viewModels {@link #subscribe(String, NotificationObserver)} + * method or when using this viewModel instance as argument to the {@link NotificationCenter#subscribe(ViewModel, String, NotificationObserver)} method. + *

+ * + * See {@link NotificationTestHelper} for a utility that's purpose is to simplify unit tests with notifications. * * @param messageName * of the notification diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java index 4724860b3..3d50d0509 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java @@ -24,6 +24,11 @@ * * {@code Object[]} with a notification. * + *

+ * + * There is a util {@link NotificationTestHelper} that's purpose is to simplify testing of notifications in Unit-Tests. + * + * * @author sialcasa * */ @@ -99,7 +104,7 @@ void unsubscribe(String messageName, * @param observer * which should execute when the notification occurs */ - void subscribe(ViewModel view, String messageName, + void subscribe(ViewModel viewModel, String messageName, NotificationObserver observer); /** @@ -118,7 +123,7 @@ void unsubscribe(ViewModel viewModel, String messageName, * @param viewModel * @param observer */ - void unsubscribe(ViewModel view, + void unsubscribe(ViewModel viewModel, NotificationObserver observer); } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java index 6aa4963ac..a800c1e9e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java @@ -31,5 +31,5 @@ public interface NotificationObserver { * @param payload * which are passed */ - public void receivedNotification(String key, Object... payload); + void receivedNotification(String key, Object... payload); } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java index 9a9c06a8c..513274ebd 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java @@ -1,5 +1,6 @@ package de.saxsys.mvvmfx.utils.notifications; +import de.saxsys.mvvmfx.ViewModel; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.util.Pair; @@ -12,27 +13,102 @@ import java.util.concurrent.TimeoutException; /** + * The {@link NotificationTestHelper} is used to simplify the testing of + * notifications. It is especially useful when notifications are send from + * different thread and when testing the direct notification between a viewModel and the View + * (via {@link ViewModel#publish(String, Object...)} and {@link ViewModel#subscribe(String, NotificationObserver)}) + *

+ * This class implements {@link NotificationObserver} and therefore can be added as subscriber. It will record + * every received notification and can be tested afterwards. + *

+ * + * The {@link ViewModel#publish(String, Object...)} method will send all notifications on the JavaFX UI thread. + * Therefore when testing the publishing of notifications JavaFX has to be running which isn't the case + * with plain JUnit tests. The {@link NotificationTestHelper} will take care for thread handling. + * + *

+ * Example: + *

+ * + *

+ *     
+ *     public class MyViewModel implements ViewModel {
+ *         public static final String ACTION_KEY = "my-action";
+ *     
+ *         public void someAction() {
+ *             ...
+ *             publish(ACTION_KEY);
+ *         }
+ *     }
+ *     
+ *     // unit test
+ *     {@code @Test}
+ *     public void testSomething() {
+ *         MyViewModel viewModel = new MyViewModel();
+ *         
+ *         NotificationTestHelper helper = new NotificationTestHelper();
+ *         viewModel.subscribe(MyViewModel.ACTION_KEY, helper);
+ *         
+ *         
+ *         viewModel.someAction();
+ *         
+ *         assertEquals(1, helper.numberOfReceivedNotifications());
+ *     }
+ * 
+ * + * + * + * You can provide a timeout as constructor parameter. + * This is useful in case of asynchronous code (f.e. when notifications are send from another Thread). + * + * By default the timeout is set to {@value #DEFAULT_TIMEOUT}. When you have a long running thread + * you should use a higher timeout. + * * @author manuel.mauky */ public class NotificationTestHelper implements NotificationObserver { + public static final long DEFAULT_TIMEOUT = 0l; + private List> notifications = new ArrayList<>(); + private long timeout = DEFAULT_TIMEOUT; + + /** + * Create a test helper with a default timeout of {@value #DEFAULT_TIMEOUT} millis. + */ public NotificationTestHelper() { new JFXPanel(); } + + /** + * Create a test helper with the given timeout in millis. + * + * @param timeoutInMillis the timeout. + */ + public NotificationTestHelper(long timeoutInMillis) { + this(); + this.timeout = timeoutInMillis; + } @Override public void receivedNotification(String key, Object... payload) { notifications.add(new Pair<>(key, payload)); } - - public int numberOfCalls() { + + /** + * @return the number of received notifications. + */ + public int numberOfReceivedNotifications() { waitForUiThread(); return notifications.size(); } - - public int numberOfCalls(String key) { + + /** + * @param key the key of the notification. + * @return the number of received notifications for the given key. + */ + public int numberOfReceivedNotifications(String key) { waitForUiThread(); return (int) notifications.stream() .filter(pair -> pair.getKey().equals(key)) @@ -42,10 +118,19 @@ public int numberOfCalls(String key) { private void waitForUiThread() { CompletableFuture future = new CompletableFuture<>(); - Platform.runLater(() -> future.complete(null)); + Platform.runLater(() -> { + if(timeout > 0) { + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + future.complete(null); + }); try { - future.get(1l, TimeUnit.SECONDS); + future.get(timeout+50, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { e.printStackTrace(); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java index 63fd68db9..75ed51fe0 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java @@ -1,18 +1,11 @@ package de.saxsys.mvvmfx.utils.notifications; -import de.saxsys.javafx.test.JfxRunner; -import de.saxsys.mvvmfx.ViewModel; +import static org.assertj.core.api.Assertions.*; + import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.assertThat; +import de.saxsys.mvvmfx.ViewModel; /** * @author manuel.mauky @@ -35,7 +28,7 @@ public void singlePublish () { viewModel.publish("test"); - assertThat(helper.numberOfCalls()).isEqualTo(1); + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); } @Test @@ -49,6 +42,60 @@ public void multiplePublish() { viewModel.publish("test"); } - assertThat(helper.numberOfCalls()).isEqualTo(n); + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(n); + } + + @Test + public void globalNotificationCenter() { + NotificationTestHelper helper = new NotificationTestHelper(); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + notificationCenter.publish("OK"); + + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } + + @Test + public void publishOnOtherThread(){ + NotificationTestHelper helper = new NotificationTestHelper(50l); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + Runnable r = () -> notificationCenter.publish("OK"); + + new Thread(r).start(); + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } + + @Test + public void timeout() { + NotificationTestHelper helper = new NotificationTestHelper(150l); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + Runnable r = () -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + e.printStackTrace(); + } + notificationCenter.publish("OK"); + }; + + new Thread(r).start(); + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); } }