Rework solution to handle individual exceptions

This commit is contained in:
Lucian Snare 2024-02-05 22:08:37 -05:00
parent 43cb6d6289
commit ec2a32838e
1 changed files with 55 additions and 15 deletions

View File

@ -1,9 +1,14 @@
package com.baeldung.concurrent.completablefuture; package com.baeldung.concurrent.completablefuture;
import static java.util.function.Predicate.isEqual;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; 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.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.ArrayList; import java.util.ArrayList;
@ -16,18 +21,19 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
public class CombiningCompletableFuturesUnitTest { public class CombiningCompletableFuturesUnitTest {
@Test @Mock Logger logger;
public void givenMicroserviceClient_whenCreateResource_thenReturnSuccess() throws ExecutionException, InterruptedException {
MicroserviceClient mockMicroserviceA = mock(MicroserviceClient.class); @BeforeEach
when(mockMicroserviceA.createResource(any())).thenReturn(CompletableFuture.completedFuture(123L)); void setup() {
CompletableFuture<Long> resultFuture = mockMicroserviceA.createResource("My Resource"); logger = mock(Logger.class);
assertEquals(123L, resultFuture.get());
} }
private static Stream<Arguments> clientData() { private static Stream<Arguments> clientData() {
@ -41,25 +47,59 @@ public class CombiningCompletableFuturesUnitTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("clientData") @MethodSource("clientData")
public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List<String> inputs, int expectedSuccess, int expectedFailure) throws ExecutionException, InterruptedException { public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List<String> inputs, int successCount, int errorCount) {
MicroserviceClient mockMicroservice = mock(MicroserviceClient.class); MicroserviceClient mockMicroservice = mock(MicroserviceClient.class);
when(mockMicroservice.createResource("Good Resource")).thenReturn(CompletableFuture.completedFuture(123L)); // Return an identifier of 123 on "Good Resource"
when(mockMicroservice.createResource("Bad Resource")).thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("Bad 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<CompletableFuture<Long>> clientCalls = new ArrayList<>(); List<CompletableFuture<Long>> clientCalls = new ArrayList<>();
for (String resource : inputs) { for (String resource : inputs) {
clientCalls.add(mockMicroservice.createResource(resource)); clientCalls.add(mockMicroservice.createResource(resource));
} }
CompletableFuture<?>[] clientCallsAsArray = clientCalls.toArray(new CompletableFuture[inputs.size()]);
CompletableFuture.allOf(clientCallsAsArray) // When all CompletableFutures are completed (exceptionally or otherwise)...
.exceptionally(ex -> null) Map<Boolean, List<Long>> 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<Long> future) {
return future
.exceptionally(ex -> handleError(ex))
.join(); .join();
Map<Boolean, List<CompletableFuture<Long>>> 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 { interface MicroserviceClient {
CompletableFuture<Long> createResource(String resourceName); CompletableFuture<Long> createResource(String resourceName);
} }
interface Logger {
void error(String message);
}
} }