From a8a79ae72ac4f2ddd5db49903f6dad679063e9b5 Mon Sep 17 00:00:00 2001 From: Vasiliy Zukanov Date: Wed, 12 Jun 2019 10:32:11 +0300 Subject: [PATCH] Changing the implementation of UiThreadPosterTestDouble such that it will not allow any posted Runnables to run before call to join(). In addition, the new implementation allows for tests with "nested" posting of Runnables to UiThreadPosterTestDouble (i.e. when one posted Runnable posts additional Runnables). This change is required in order to prevent some code to be executed while the tests that use UiThreadPosterTestDouble are still being set up, thus leading to inconsistent results. Now, it's guaranteed that no side effects of Runnables posted UiThreadPosterTestDouble will be visible until a call to join(). --- .../testdoubles/UiThreadPosterTestDouble.java | 58 ++++----- .../UiThreadPosterTestDoubleTest.java | 110 ++++++++---------- 2 files changed, 69 insertions(+), 99 deletions(-) diff --git a/threadposter/src/main/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDouble.java b/threadposter/src/main/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDouble.java index 678c84a..11cda79 100755 --- a/threadposter/src/main/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDouble.java +++ b/threadposter/src/main/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDouble.java @@ -6,20 +6,18 @@ import java.util.LinkedList; import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Test double of {@link UiThreadPoster} that can be used in tests in order to establish * a happens-before relationship between any {@link Runnable} sent to execution and subsequent * test assertions. - * Instead of using Android's UI (aka main) thread, this implementation sends each {@link Runnable} - * to a new background thread. Only one background thread is allowed to run at a time, thus - * simulating a serial execution of {@link Runnable}s. + * Instead of using Android's UI (aka main) thread, this implementation runs all {@link Runnable}s + * on a single background thread in order, thus simulating serial execution on UI thread. */ /* pp */ class UiThreadPosterTestDouble extends UiThreadPoster { - private final Object MONITOR = new Object(); - - private final Queue mThreads = new LinkedList<>(); + private final Queue mRunnables = new ConcurrentLinkedQueue<>(); @Override protected Handler getMainHandler() { @@ -30,47 +28,31 @@ protected Handler getMainHandler() { @Override public void post(final Runnable runnable) { - synchronized (MONITOR) { - Thread worker = new Thread(new Runnable() { - @Override - public void run() { - // make sure all previous threads finished - UiThreadPosterTestDouble.this.join(); - runnable.run(); - } - }); - mThreads.add(worker); - worker.start(); - } + mRunnables.add(runnable); } /** - * Call to this method will block until all {@link Runnable}s posted to this "test double" - * BEFORE THE MOMENT OF A CALL will be completed.
+ * Execute all {@link Runnable}s posted to this "test double". The caller will block until the operation completes
* Call to this method allows to establish a happens-before relationship between the previously * posted {@link Runnable}s and subsequent code. */ public void join() { - Queue threadsCopy; - synchronized (MONITOR) { - threadsCopy = new LinkedList<>(mThreads); - } - - Thread thread; - while ((thread = threadsCopy.poll()) != null) { - try { - - // due to the way post(Runnable) is being implemented, this method will be called - // by threads that were added to the queue; in this case, we need to join only on - // threads that precede the calling thread in the queue - if (thread.getId() == Thread.currentThread().getId()) { - break; - } else { - thread.join(); + final Thread fakeUiThread = new Thread() { + @Override + public void run() { + Runnable runnable; + while ((runnable = mRunnables.poll()) != null) { + runnable.run(); } - } catch (InterruptedException e) { - e.printStackTrace(); } + }; + + fakeUiThread.start(); + + try { + fakeUiThread.join(); + } catch (InterruptedException e) { + throw new RuntimeException("interrupted"); } } diff --git a/threadposter/src/test/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDoubleTest.java b/threadposter/src/test/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDoubleTest.java index 3179280..aa48e0a 100755 --- a/threadposter/src/test/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDoubleTest.java +++ b/threadposter/src/test/java/com/techyourchance/threadposter/testdoubles/UiThreadPosterTestDoubleTest.java @@ -10,24 +10,7 @@ public class UiThreadPosterTestDoubleTest { - private static final int TEST_TIMEOUT_MS = 1000; - private static final int TEST_DELAY_MS = TEST_TIMEOUT_MS / 10; - - /** - * This class will be used in order to check side effects in tests - */ - private class Appender { - - private String mString = ""; - - private void append(String string) { - mString += string; - } - - private String getString() { - return mString; - } - } + private static final int TEST_DELAY_MS = 10; private UiThreadPosterTestDouble SUT; @@ -43,11 +26,6 @@ public void executeThenJoin_singleRunnable_sideEffectNotVisibleBeforeJoin() thro Runnable runnable = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("a"); } }; @@ -66,18 +44,12 @@ public void executeThenJoin_singleRunnable_sideEffectsVisibleAfterJoin() throws Runnable runnable = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("a"); } }; // Act SUT.post(runnable); // Assert - Thread.sleep(TEST_DELAY_MS); SUT.join(); assertThat(appender.getString(), is("a")); } @@ -90,33 +62,18 @@ public void executeThenJoin_multipleRunnables_sideEffectsNotVisibleBeforeJoin() Runnable runnable1 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("a"); } }; Runnable runnable2 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("b"); } }; Runnable runnable3 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("c"); } }; @@ -129,7 +86,6 @@ public void run() { assertThat(appender.getString(), is("")); } - @Test public void executeThenJoin_multipleRunnables_sideEffectsVisibleAfterJoinInOrder() throws Exception { // Arrange @@ -137,33 +93,18 @@ public void executeThenJoin_multipleRunnables_sideEffectsVisibleAfterJoinInOrder Runnable runnable1 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("a"); } }; Runnable runnable2 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("b"); } }; Runnable runnable3 = new Runnable() { @Override public void run() { - try { - Thread.sleep(2 * TEST_DELAY_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } appender.append("c"); } }; @@ -172,8 +113,55 @@ public void run() { SUT.post(runnable2); SUT.post(runnable3); // Assert - Thread.sleep(TEST_DELAY_MS); SUT.join(); assertThat(appender.getString(), is("abc")); } + + + @Test + public void executeThenJoin_multipleNestedRunnables_sideEffectsVisibleAfterJoinInReversedOrder() throws Exception { + // Arrange + final Appender appender = new Appender(); + final Runnable runnable1 = new Runnable() { + @Override + public void run() { + appender.append("a"); + } + }; + final Runnable runnable2 = new Runnable() { + @Override + public void run() { + SUT.post(runnable1); + appender.append("b"); + } + }; + final Runnable runnable3 = new Runnable() { + @Override + public void run() { + SUT.post(runnable2); + appender.append("c"); + } + }; + // Act + SUT.post(runnable3); + // Assert + SUT.join(); + assertThat(appender.getString(), is("cba")); + } + + /** + * This class will be used in order to check side effects in tests + */ + private class Appender { + + private String mString = ""; + + private synchronized void append(String string) { + mString += string; + } + + private synchronized String getString() { + return mString; + } + } } \ No newline at end of file