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.
By the end of this module, you will be able to:
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));
@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");
});
}
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());
}
}
@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);
}
@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());
}
@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:"));
}
@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());
}
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();
}
}
}
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);
}
}
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();
}
}
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);
});
}
}