Previous: Optional Class for Null Handling Next: Reflection API for Dynamic Test Frameworks

Module 5: CompletableFuture for Asynchronous Operations

Overview

CompletableFuture, introduced in Java 8, provides a powerful framework for asynchronous programming. In test automation, asynchronous operations are common when dealing with web services, UI interactions, and parallel test execution. This module explores how to leverage CompletableFuture to create more efficient, responsive, and maintainable test automation solutions.

Learning Objectives

By the end of this module, you will be able to:

  1. Understand the fundamentals of asynchronous programming with CompletableFuture
  2. Create and chain asynchronous operations
  3. Handle exceptions in asynchronous code
  4. Implement timeout and cancellation strategies
  5. Apply CompletableFuture in common test automation scenarios

5.1 Introduction to Asynchronous Programming

Synchronous vs. Asynchronous Execution

In synchronous execution, operations are performed sequentially, with each operation blocking until it completes:

// Synchronous execution
result1 = operation1();
result2 = operation2(result1);
result3 = operation3(result2);

In asynchronous execution, operations can be initiated without waiting for completion:

// Asynchronous execution
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> operation1());
CompletableFuture<String> future2 = future1.thenApplyAsync(result -> operation2(result));
CompletableFuture<String> future3 = future2.thenApplyAsync(result -> operation3(result));

5.2 Creating CompletableFuture

Basic Creation Methods

@Test
public void testCompletableFutureCreation() {
    // Create completed future
    CompletableFuture<String> completed = CompletableFuture.completedFuture("Hello");
    assertEquals("Hello", completed.join());
    
    // Create future with supplier
    CompletableFuture<String> supplied = CompletableFuture.supplyAsync(() -> {
        // Simulate API call
        return "API Response";
    });
    
    // Create future with runnable (no return value)
    CompletableFuture<Void> runnable = CompletableFuture.runAsync(() -> {
        System.out.println("Background task completed");
    });
}

Test Automation Example

public class AsyncTestOperations {
    
    @Test
    public void testParallelApiCalls() {
        // Execute multiple API calls in parallel
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> 
            callUserApi("user123"));
        
        CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> 
            callOrderApi("order456"));
        
        CompletableFuture<String> inventoryFuture = CompletableFuture.supplyAsync(() -> 
            callInventoryApi("item789"));
        
        // Wait for all to complete
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
            userFuture, orderFuture, inventoryFuture);
        
        allFutures.join(); // Wait for completion
        
        // Verify results
        assertNotNull(userFuture.join());
        assertNotNull(orderFuture.join());
        assertNotNull(inventoryFuture.join());
    }
}

5.3 Chaining Operations

Transformation Methods

@Test
public void testChainingOperations() {
    CompletableFuture<String> result = CompletableFuture
        .supplyAsync(() -> "user123")
        .thenApply(userId -> fetchUserData(userId))
        .thenApply(userData -> processUserData(userData))
        .thenApply(processedData -> generateReport(processedData));
    
    String finalResult = result.join();
    assertNotNull(finalResult);
}

@Test
public void testAsyncChaining() {
    CompletableFuture<String> result = CompletableFuture
        .supplyAsync(() -> "user123")
        .thenApplyAsync(userId -> fetchUserData(userId))
        .thenApplyAsync(userData -> processUserData(userData))
        .thenApplyAsync(processedData -> generateReport(processedData));
    
    String finalResult = result.join();
    assertNotNull(finalResult);
}

5.4 Combining Futures

Combining Two Futures

@Test
public void testCombiningFutures() {
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
    
    // Combine two futures
    CompletableFuture<String> combined = future1.thenCombine(future2, 
        (result1, result2) -> result1 + " " + result2);
    
    assertEquals("Hello World", combined.join());
}

@Test
public void testCombiningMultipleFutures() {
    List<CompletableFuture<String>> futures = Arrays.asList(
        CompletableFuture.supplyAsync(() -> callService1()),
        CompletableFuture.supplyAsync(() -> callService2()),
        CompletableFuture.supplyAsync(() -> callService3())
    );
    
    // Wait for all to complete and collect results
    CompletableFuture<List<String>> allResults = CompletableFuture
        .allOf(futures.toArray(new CompletableFuture[0]))
        .thenApply(v -> futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList()));
    
    List<String> results = allResults.join();
    assertEquals(3, results.size());
}

5.5 Exception Handling

Handling Exceptions

@Test
public void testExceptionHandling() {
    CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> {
            if (Math.random() > 0.5) {
                throw new RuntimeException("Random failure");
            }
            return "Success";
        })
        .exceptionally(throwable -> {
            System.err.println("Exception occurred: " + throwable.getMessage());
            return "Default value";
        });
    
    String result = future.join();
    assertNotNull(result);
}

@Test
public void testExceptionHandlingWithHandle() {
    CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> {
            throw new RuntimeException("Test exception");
        })
        .handle((result, throwable) -> {
            if (throwable != null) {
                return "Error: " + throwable.getMessage();
            }
            return result;
        });
    
    String result = future.join();
    assertTrue(result.startsWith("Error:"));
}

5.6 Timeouts and Cancellation

Implementing Timeouts

@Test
public void testTimeout() {
    CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> {
            try {
                Thread.sleep(5000); // Simulate long operation
                return "Completed";
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        })
        .orTimeout(2, TimeUnit.SECONDS)
        .exceptionally(throwable -> {
            if (throwable instanceof TimeoutException) {
                return "Operation timed out";
            }
            return "Error occurred";
        });
    
    String result = future.join();
    assertEquals("Operation timed out", result);
}

@Test
public void testCancellation() {
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(5000);
            return "Completed";
        } catch (InterruptedException e) {
            return "Interrupted";
        }
    });
    
    // Cancel after 1 second
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    scheduler.schedule(() -> future.cancel(true), 1, TimeUnit.SECONDS);
    
    assertTrue(future.isCancelled());
}

5.7 Practical Test Automation Scenarios

Parallel UI Testing

public class ParallelUITests {
    
    @Test
    public void testParallelBrowserOperations() {
        List<String> urls = Arrays.asList(
            "https://example1.com",
            "https://example2.com", 
            "https://example3.com"
        );
        
        List<CompletableFuture<String>> futures = urls.stream()
            .map(url -> CompletableFuture.supplyAsync(() -> testUrl(url)))
            .collect(Collectors.toList());
        
        CompletableFuture<Void> allTests = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]));
        
        allTests.join();
        
        // Verify all tests passed
        futures.forEach(future -> {
            String result = future.join();
            assertEquals("PASSED", result);
        });
    }
    
    private String testUrl(String url) {
        // Simulate browser test
        WebDriver driver = new ChromeDriver();
        try {
            driver.get(url);
            // Perform test operations
            return "PASSED";
        } finally {
            driver.quit();
        }
    }
}

Asynchronous API Testing

public class AsyncApiTests {
    
    @Test
    public void testAsyncApiWorkflow() {
        CompletableFuture<String> workflow = CompletableFuture
            .supplyAsync(() -> createUser())
            .thenCompose(userId -> 
                CompletableFuture.supplyAsync(() -> createOrder(userId)))
            .thenCompose(orderId -> 
                CompletableFuture.supplyAsync(() -> processPayment(orderId)))
            .thenCompose(paymentId -> 
                CompletableFuture.supplyAsync(() -> sendConfirmation(paymentId)))
            .exceptionally(throwable -> {
                System.err.println("Workflow failed: " + throwable.getMessage());
                return "FAILED";
            });
        
        String result = workflow.join();
        assertNotEquals("FAILED", result);
    }
    
    @Test
    public void testApiLoadTesting() {
        int numberOfRequests = 100;
        List<CompletableFuture<Long>> futures = IntStream.range(0, numberOfRequests)
            .mapToObj(i -> CompletableFuture.supplyAsync(() -> {
                long startTime = System.currentTimeMillis();
                callApi();
                return System.currentTimeMillis() - startTime;
            }))
            .collect(Collectors.toList());
        
        CompletableFuture<Void> allRequests = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]));
        
        allRequests.join();
        
        // Calculate average response time
        double averageTime = futures.stream()
            .mapToLong(CompletableFuture::join)
            .average()
            .orElse(0.0);
        
        assertTrue("Average response time should be under 1000ms", 
                   averageTime < 1000);
    }
}

5.8 Best Practices

Thread Pool Management

public class AsyncTestManager {
    private final ExecutorService customExecutor = 
        Executors.newFixedThreadPool(10);
    
    @Test
    public void testWithCustomExecutor() {
        CompletableFuture<String> future = CompletableFuture
            .supplyAsync(() -> performOperation(), customExecutor)
            .thenApplyAsync(result -> processResult(result), customExecutor);
        
        String result = future.join();
        assertNotNull(result);
    }
    
    @AfterEach
    public void cleanup() {
        customExecutor.shutdown();
    }
}

Error Handling Strategies

public class RobustAsyncTesting {
    
    @Test
    public void testWithRetry() {
        CompletableFuture<String> future = retryAsync(() -> 
            unreliableOperation(), 3);
        
        String result = future.join();
        assertNotNull(result);
    }
    
    private CompletableFuture<String> retryAsync(
            Supplier<String> operation, int maxRetries) {
        return CompletableFuture.supplyAsync(operation)
            .exceptionally(throwable -> {
                if (maxRetries > 0) {
                    return retryAsync(operation, maxRetries - 1).join();
                }
                throw new RuntimeException("Max retries exceeded", throwable);
            });
    }
}
Previous: Optional Class for Null Handling Next: Reflection API for Dynamic Test Frameworks