From c90eac4ac58ea3d507f602a39dc964a631693575 Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Sat, 3 Feb 2024 22:30:22 -0500 Subject: [PATCH 1/6] Add unit test to resolve multiple CompletableFutures --- .../core-java-concurrency-2/pom.xml | 4 +- .../CombiningCompletableFuturesUnitTest.java | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java diff --git a/core-java-modules/core-java-concurrency-2/pom.xml b/core-java-modules/core-java-concurrency-2/pom.xml index e373c829cc..b27d268c5a 100644 --- a/core-java-modules/core-java-concurrency-2/pom.xml +++ b/core-java-modules/core-java-concurrency-2/pom.xml @@ -60,8 +60,8 @@ 3.1 ${javac.target} - ${java.version} - ${java.version} + 9 + 9 diff --git a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java new file mode 100644 index 0000000000..a12c0c88d1 --- /dev/null +++ b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java @@ -0,0 +1,62 @@ +package com.baeldung.concurrent.completablefuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class CombiningCompletableFuturesUnitTest { + + @Test + public void givenMicroserviceClient_whenCreateResource_thenReturnSuccess() throws ExecutionException, InterruptedException { + MicroserviceClient mockMicroserviceA = mock(MicroserviceClient.class); + when(mockMicroserviceA.createResource(any())).thenReturn(CompletableFuture.completedFuture(123L)); + CompletableFuture resultFuture = mockMicroserviceA.createResource("My Resource"); + assertEquals(123L, resultFuture.get()); + } + + private static Stream givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults() { + return Stream.of( + Arguments.of(List.of("Good Resource"), 1, 0), + Arguments.of(List.of("Bad Resource"), 0, 1), + Arguments.of(List.of("Good Resource", "Bad Resource"), 1, 1), + Arguments.of(List.of("Good Resource", "Bad Resource", "Good Resource", "Bad Resource", "Good Resource"), 3, 2) + ); + } + + @ParameterizedTest + @MethodSource + public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List inputs, int expectedSuccess, int expectedFailure) throws ExecutionException, InterruptedException { + MicroserviceClient mockMicroservice = mock(MicroserviceClient.class); + when(mockMicroservice.createResource("Good Resource")).thenReturn(CompletableFuture.completedFuture(123L)); + when(mockMicroservice.createResource("Bad Resource")).thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("Bad Resource"))); + + List> clientCalls = new ArrayList<>(); + for (String resource : inputs) { + clientCalls.add(mockMicroservice.createResource(resource)); + } + CompletableFuture.allOf(clientCalls.toArray(new CompletableFuture[inputs.size()])).exceptionally(ex -> null).join(); + Map>> resultsByFailure = clientCalls.stream().collect(Collectors.partitioningBy(CompletableFuture::isCompletedExceptionally)); + assertThat(resultsByFailure.getOrDefault(false, Collections.emptyList()).size()).isEqualTo(expectedSuccess); + assertThat(resultsByFailure.getOrDefault(true, Collections.emptyList()).size()).isEqualTo(expectedFailure); + } + + interface MicroserviceClient { + CompletableFuture createResource(String resourceName); + } +} From 521ec4f4fc1fe685f7784233a7a91f0a5df33a21 Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Sun, 4 Feb 2024 00:44:31 -0500 Subject: [PATCH 2/6] A few small test updates --- .../CombiningCompletableFuturesUnitTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java index a12c0c88d1..d52d8eef16 100644 --- a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java +++ b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java @@ -30,7 +30,7 @@ public class CombiningCompletableFuturesUnitTest { assertEquals(123L, resultFuture.get()); } - private static Stream givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults() { + private static Stream clientData() { return Stream.of( Arguments.of(List.of("Good Resource"), 1, 0), Arguments.of(List.of("Bad Resource"), 0, 1), @@ -40,7 +40,7 @@ public class CombiningCompletableFuturesUnitTest { } @ParameterizedTest - @MethodSource + @MethodSource("clientData") public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List inputs, int expectedSuccess, int expectedFailure) throws ExecutionException, InterruptedException { MicroserviceClient mockMicroservice = mock(MicroserviceClient.class); when(mockMicroservice.createResource("Good Resource")).thenReturn(CompletableFuture.completedFuture(123L)); @@ -50,7 +50,10 @@ public class CombiningCompletableFuturesUnitTest { for (String resource : inputs) { clientCalls.add(mockMicroservice.createResource(resource)); } - CompletableFuture.allOf(clientCalls.toArray(new CompletableFuture[inputs.size()])).exceptionally(ex -> null).join(); + CompletableFuture[] clientCallsAsArray = clientCalls.toArray(new CompletableFuture[inputs.size()]); + CompletableFuture.allOf(clientCallsAsArray) + .exceptionally(ex -> null) + .join(); Map>> resultsByFailure = clientCalls.stream().collect(Collectors.partitioningBy(CompletableFuture::isCompletedExceptionally)); assertThat(resultsByFailure.getOrDefault(false, Collections.emptyList()).size()).isEqualTo(expectedSuccess); assertThat(resultsByFailure.getOrDefault(true, Collections.emptyList()).size()).isEqualTo(expectedFailure); From 43cb6d628980e77678580c81acbaff5381550220 Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Sun, 4 Feb 2024 00:52:15 -0500 Subject: [PATCH 3/6] Remove java version change --- core-java-modules/core-java-concurrency-2/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-java-modules/core-java-concurrency-2/pom.xml b/core-java-modules/core-java-concurrency-2/pom.xml index b27d268c5a..e373c829cc 100644 --- a/core-java-modules/core-java-concurrency-2/pom.xml +++ b/core-java-modules/core-java-concurrency-2/pom.xml @@ -60,8 +60,8 @@ 3.1 ${javac.target} - 9 - 9 + ${java.version} + ${java.version} From ec2a32838ee4ed19ccacf4695141fa5c9969ff9b Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Mon, 5 Feb 2024 22:08:37 -0500 Subject: [PATCH 4/6] Rework solution to handle individual exceptions --- .../CombiningCompletableFuturesUnitTest.java | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java index d52d8eef16..48d647c487 100644 --- a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java +++ b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java @@ -1,9 +1,14 @@ package com.baeldung.concurrent.completablefuture; +import static java.util.function.Predicate.isEqual; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -16,18 +21,19 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; public class CombiningCompletableFuturesUnitTest { - @Test - public void givenMicroserviceClient_whenCreateResource_thenReturnSuccess() throws ExecutionException, InterruptedException { - MicroserviceClient mockMicroserviceA = mock(MicroserviceClient.class); - when(mockMicroserviceA.createResource(any())).thenReturn(CompletableFuture.completedFuture(123L)); - CompletableFuture resultFuture = mockMicroserviceA.createResource("My Resource"); - assertEquals(123L, resultFuture.get()); + @Mock Logger logger; + + @BeforeEach + void setup() { + logger = mock(Logger.class); } private static Stream clientData() { @@ -41,25 +47,59 @@ public class CombiningCompletableFuturesUnitTest { @ParameterizedTest @MethodSource("clientData") - public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List inputs, int expectedSuccess, int expectedFailure) throws ExecutionException, InterruptedException { + public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List inputs, int successCount, int errorCount) { MicroserviceClient mockMicroservice = mock(MicroserviceClient.class); - when(mockMicroservice.createResource("Good Resource")).thenReturn(CompletableFuture.completedFuture(123L)); - when(mockMicroservice.createResource("Bad Resource")).thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("Bad Resource"))); + // Return an identifier of 123 on "Good Resource" + when(mockMicroservice.createResource("Good Resource")) + .thenReturn(CompletableFuture.completedFuture(123L)); + // Throw an exception on "Bad Resource" + when(mockMicroservice.createResource("Bad Resource")) + .thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("Bad Resource"))); + // Given a list of CompletableFutures from our microservice calls... List> clientCalls = new ArrayList<>(); for (String resource : inputs) { clientCalls.add(mockMicroservice.createResource(resource)); } - CompletableFuture[] clientCallsAsArray = clientCalls.toArray(new CompletableFuture[inputs.size()]); - CompletableFuture.allOf(clientCallsAsArray) - .exceptionally(ex -> null) + + // When all CompletableFutures are completed (exceptionally or otherwise)... + Map> resultsByValidity = clientCalls.stream() + .map(future -> handleFuture(future)) + .collect(Collectors.partitioningBy(resourceId -> isValidResponse(resourceId))); + + // Then the returned resource identifiers should match what is expected... + assertThat(resultsByValidity.getOrDefault(true, List.of()).size()).isEqualTo(successCount); + // And the logger mock should be called once for each exception with the expected error message + assertThat(resultsByValidity.getOrDefault(false, List.of()).size()).isEqualTo(errorCount); + verify(logger, times(errorCount)) + .error(eq("Encountered error: java.lang.IllegalArgumentException: Bad Resource")); + } + + private boolean isValidResponse(long resourceId) { + return resourceId != -1L; + } + + /** + * Completes the given CompletableFuture, handling any exceptions that are thrown. + * @param future the CompletableFuture to complete. + * @return the resource identifier (-1 if the request failed). + */ + private Long handleFuture(CompletableFuture future) { + return future + .exceptionally(ex -> handleError(ex)) .join(); - Map>> resultsByFailure = clientCalls.stream().collect(Collectors.partitioningBy(CompletableFuture::isCompletedExceptionally)); - assertThat(resultsByFailure.getOrDefault(false, Collections.emptyList()).size()).isEqualTo(expectedSuccess); - assertThat(resultsByFailure.getOrDefault(true, Collections.emptyList()).size()).isEqualTo(expectedFailure); + } + + private Long handleError(Throwable throwable) { + logger.error("Encountered error: " + throwable); + return -1L; } interface MicroserviceClient { CompletableFuture createResource(String resourceName); } + + interface Logger { + void error(String message); + } } From 195a0e06d7624ad4eee62157cdb49f8c07c14ec8 Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Thu, 8 Feb 2024 23:05:59 -0500 Subject: [PATCH 5/6] Fix suggestions --- .../CombiningCompletableFuturesUnitTest.java | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java index 48d647c487..dbf8b98092 100644 --- a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java +++ b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java @@ -1,10 +1,6 @@ package com.baeldung.concurrent.completablefuture; -import static java.util.function.Predicate.isEqual; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -12,29 +8,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.junit.Test; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; public class CombiningCompletableFuturesUnitTest { - @Mock Logger logger; - - @BeforeEach - void setup() { - logger = mock(Logger.class); - } + private final Logger logger = mock(Logger.class); private static Stream clientData() { return Stream.of( @@ -64,13 +50,16 @@ public class CombiningCompletableFuturesUnitTest { // When all CompletableFutures are completed (exceptionally or otherwise)... Map> resultsByValidity = clientCalls.stream() - .map(future -> handleFuture(future)) - .collect(Collectors.partitioningBy(resourceId -> isValidResponse(resourceId))); + .map(this::handleFuture) + .collect(Collectors.partitioningBy(this::isValidResponse)); // Then the returned resource identifiers should match what is expected... - assertThat(resultsByValidity.getOrDefault(true, List.of()).size()).isEqualTo(successCount); + List validResults = resultsByValidity.getOrDefault(true, List.of()); + assertThat(validResults.size()).isEqualTo(successCount); + // And the logger mock should be called once for each exception with the expected error message - assertThat(resultsByValidity.getOrDefault(false, List.of()).size()).isEqualTo(errorCount); + List invalidResults = resultsByValidity.getOrDefault(false, List.of()); + assertThat(invalidResults.size()).isEqualTo(errorCount); verify(logger, times(errorCount)) .error(eq("Encountered error: java.lang.IllegalArgumentException: Bad Resource")); } @@ -86,7 +75,7 @@ public class CombiningCompletableFuturesUnitTest { */ private Long handleFuture(CompletableFuture future) { return future - .exceptionally(ex -> handleError(ex)) + .exceptionally(this::handleError) .join(); } From cd1c8de7355afbcca23f22adf90d344cc96a586e Mon Sep 17 00:00:00 2001 From: Lucian Snare Date: Thu, 8 Feb 2024 23:23:51 -0500 Subject: [PATCH 6/6] Fix suggestions --- .../CombiningCompletableFuturesUnitTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java index dbf8b98092..2c157b3ab2 100644 --- a/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java +++ b/core-java-modules/core-java-concurrency-2/src/test/java/com/baeldung/concurrent/completablefuture/CombiningCompletableFuturesUnitTest.java @@ -51,7 +51,7 @@ public class CombiningCompletableFuturesUnitTest { // When all CompletableFutures are completed (exceptionally or otherwise)... Map> resultsByValidity = clientCalls.stream() .map(this::handleFuture) - .collect(Collectors.partitioningBy(this::isValidResponse)); + .collect(Collectors.partitioningBy(resourceId -> resourceId != -1L)); // Then the returned resource identifiers should match what is expected... List validResults = resultsByValidity.getOrDefault(true, List.of()); @@ -64,10 +64,6 @@ public class CombiningCompletableFuturesUnitTest { .error(eq("Encountered error: java.lang.IllegalArgumentException: Bad Resource")); } - private boolean isValidResponse(long resourceId) { - return resourceId != -1L; - } - /** * Completes the given CompletableFuture, handling any exceptions that are thrown. * @param future the CompletableFuture to complete.