diff --git a/async-method-invocation/pom.xml b/async-method-invocation/pom.xml index be6aa3bca..de81eb0b7 100644 --- a/async-method-invocation/pom.xml +++ b/async-method-invocation/pom.xml @@ -14,5 +14,10 @@ junit test + + org.mockito + mockito-core + test + diff --git a/async-method-invocation/src/test/java/com/iluwatar/async/method/invocation/ThreadAsyncExecutorTest.java b/async-method-invocation/src/test/java/com/iluwatar/async/method/invocation/ThreadAsyncExecutorTest.java new file mode 100644 index 000000000..7db0abfce --- /dev/null +++ b/async-method-invocation/src/test/java/com/iluwatar/async/method/invocation/ThreadAsyncExecutorTest.java @@ -0,0 +1,301 @@ +package com.iluwatar.async.method.invocation; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +/** + * Date: 12/6/15 - 10:49 AM + * + * @author Jeroen Meulemeester + */ +public class ThreadAsyncExecutorTest { + + /** + * Test used to verify the happy path of {@link ThreadAsyncExecutor#startProcess(Callable)} + */ + @Test(timeout = 3000) + public void testSuccessfulTaskWithoutCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + + final Object result = new Object(); + final Callable task = mock(Callable.class); + when(task.call()).thenReturn(result); + + final AsyncResult asyncResult = executor.startProcess(task); + assertNotNull(asyncResult); + asyncResult.await(); // Prevent timing issues, and wait until the result is available + assertTrue(asyncResult.isCompleted()); + + // Our task should only execute once ... + verify(task, times(1)).call(); + + // ... and the result should be exactly the same object + assertSame(result, asyncResult.getValue()); + } + + /** + * Test used to verify the happy path of {@link ThreadAsyncExecutor#startProcess(Callable, AsyncCallback)} + */ + @Test(timeout = 3000) + public void testSuccessfulTaskWithCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + + final Object result = new Object(); + final Callable task = mock(Callable.class); + when(task.call()).thenReturn(result); + + final AsyncCallback callback = mock(AsyncCallback.class); + final AsyncResult asyncResult = executor.startProcess(task, callback); + assertNotNull(asyncResult); + asyncResult.await(); // Prevent timing issues, and wait until the result is available + assertTrue(asyncResult.isCompleted()); + + // Our task should only execute once ... + verify(task, times(1)).call(); + + // ... same for the callback, we expect our object + final ArgumentCaptor> optionalCaptor = ArgumentCaptor.forClass((Class) Optional.class); + verify(callback, times(1)).onComplete(eq(result), optionalCaptor.capture()); + + final Optional optionalException = optionalCaptor.getValue(); + assertNotNull(optionalException); + assertFalse(optionalException.isPresent()); + + // ... and the result should be exactly the same object + assertSame(result, asyncResult.getValue()); + } + + /** + * Test used to verify the happy path of {@link ThreadAsyncExecutor#startProcess(Callable)} + * when a task takes a while to execute + */ + @Test(timeout = 5000) + public void testLongRunningTaskWithoutCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + + final Object result = new Object(); + final Callable task = mock(Callable.class); + when(task.call()).thenAnswer(i -> { + Thread.sleep(1500); + return result; + }); + + final AsyncResult asyncResult = executor.startProcess(task); + assertNotNull(asyncResult); + assertFalse(asyncResult.isCompleted()); + + try { + asyncResult.getValue(); + fail("Expected IllegalStateException when calling AsyncResult#getValue on a non-completed task"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } + + // Our task should only execute once, but it can take a while ... + verify(task, timeout(3000).times(1)).call(); + + // Prevent timing issues, and wait until the result is available + asyncResult.await(); + assertTrue(asyncResult.isCompleted()); + verifyNoMoreInteractions(task); + + // ... and the result should be exactly the same object + assertSame(result, asyncResult.getValue()); + } + + /** + * Test used to verify the happy path of {@link ThreadAsyncExecutor#startProcess(Callable, AsyncCallback)} + * when a task takes a while to execute + */ + @Test(timeout = 5000) + public void testLongRunningTaskWithCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + + final Object result = new Object(); + final Callable task = mock(Callable.class); + when(task.call()).thenAnswer(i -> { + Thread.sleep(1500); + return result; + }); + + final AsyncCallback callback = mock(AsyncCallback.class); + final AsyncResult asyncResult = executor.startProcess(task, callback); + assertNotNull(asyncResult); + assertFalse(asyncResult.isCompleted()); + + verifyZeroInteractions(callback); + + try { + asyncResult.getValue(); + fail("Expected IllegalStateException when calling AsyncResult#getValue on a non-completed task"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } + + // Our task should only execute once, but it can take a while ... + verify(task, timeout(3000).times(1)).call(); + + final ArgumentCaptor> optionalCaptor = ArgumentCaptor.forClass((Class) Optional.class); + verify(callback, timeout(3000).times(1)).onComplete(eq(result), optionalCaptor.capture()); + + final Optional optionalException = optionalCaptor.getValue(); + assertNotNull(optionalException); + assertFalse(optionalException.isPresent()); + + // Prevent timing issues, and wait until the result is available + asyncResult.await(); + assertTrue(asyncResult.isCompleted()); + verifyNoMoreInteractions(task, callback); + + // ... and the result should be exactly the same object + assertSame(result, asyncResult.getValue()); + } + + /** + * Test used to verify the happy path of {@link ThreadAsyncExecutor#startProcess(Callable)} + * when a task takes a while to execute, while waiting on the result using {@link ThreadAsyncExecutor#endProcess(AsyncResult)} + */ + @Test(timeout = 5000) + public void testEndProcess() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + + final Object result = new Object(); + final Callable task = mock(Callable.class); + when(task.call()).thenAnswer(i -> { + Thread.sleep(1500); + return result; + }); + + final AsyncResult asyncResult = executor.startProcess(task); + assertNotNull(asyncResult); + assertFalse(asyncResult.isCompleted()); + + try { + asyncResult.getValue(); + fail("Expected IllegalStateException when calling AsyncResult#getValue on a non-completed task"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } + + assertSame(result, executor.endProcess(asyncResult)); + verify(task, times(1)).call(); + assertTrue(asyncResult.isCompleted()); + + // Calling end process a second time while already finished should give the same result + assertSame(result, executor.endProcess(asyncResult)); + verifyNoMoreInteractions(task); + } + + /** + * Test used to verify the behaviour of {@link ThreadAsyncExecutor#startProcess(Callable)} + * when the callable is 'null' + */ + @Test(timeout = 3000) + public void testNullTask() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + final AsyncResult asyncResult = executor.startProcess(null); + + assertNotNull("The AsyncResult should not be 'null', even though the task was 'null'.", asyncResult); + asyncResult.await(); // Prevent timing issues, and wait until the result is available + assertTrue(asyncResult.isCompleted()); + + try { + asyncResult.getValue(); + fail("Expected ExecutionException with NPE as cause"); + } catch (final ExecutionException e) { + assertNotNull(e.getMessage()); + assertNotNull(e.getCause()); + assertEquals(NullPointerException.class, e.getCause().getClass()); + } + + } + + /** + * Test used to verify the behaviour of {@link ThreadAsyncExecutor#startProcess(Callable, AsyncCallback)} + * when the callable is 'null', but the asynchronous callback is provided + */ + @Test(timeout = 3000) + public void testNullTaskWithCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + final AsyncCallback callback = mock(AsyncCallback.class); + final AsyncResult asyncResult = executor.startProcess(null, callback); + + assertNotNull("The AsyncResult should not be 'null', even though the task was 'null'.", asyncResult); + asyncResult.await(); // Prevent timing issues, and wait until the result is available + assertTrue(asyncResult.isCompleted()); + + final ArgumentCaptor> optionalCaptor = ArgumentCaptor.forClass((Class) Optional.class); + verify(callback, times(1)).onComplete(Matchers.isNull(), optionalCaptor.capture()); + + final Optional optionalException = optionalCaptor.getValue(); + assertNotNull(optionalException); + assertTrue(optionalException.isPresent()); + + final Exception exception = optionalException.get(); + assertNotNull(exception); + assertEquals(NullPointerException.class, exception.getClass()); + + try { + asyncResult.getValue(); + fail("Expected ExecutionException with NPE as cause"); + } catch (final ExecutionException e) { + assertNotNull(e.getMessage()); + assertNotNull(e.getCause()); + assertEquals(NullPointerException.class, e.getCause().getClass()); + } + + } + + /** + * Test used to verify the behaviour of {@link ThreadAsyncExecutor#startProcess(Callable, AsyncCallback)} + * when both the callable and the asynchronous callback are 'null' + */ + @Test(timeout = 3000) + public void testNullTaskWithNullCallback() throws Exception { + // Instantiate a new executor and start a new 'null' task ... + final ThreadAsyncExecutor executor = new ThreadAsyncExecutor(); + final AsyncResult asyncResult = executor.startProcess(null, null); + + assertNotNull("The AsyncResult should not be 'null', even though the task and callback were 'null'.", asyncResult); + asyncResult.await(); // Prevent timing issues, and wait until the result is available + assertTrue(asyncResult.isCompleted()); + + try { + asyncResult.getValue(); + fail("Expected ExecutionException with NPE as cause"); + } catch (final ExecutionException e) { + assertNotNull(e.getMessage()); + assertNotNull(e.getCause()); + assertEquals(NullPointerException.class, e.getCause().getClass()); + } + + } + +} \ No newline at end of file