Mastering Dynamic Waits in Test Automation with Callable and Functional Interfaces

Raviteja GuttiRaviteja Gutti
7 min read

In any automation framework, waiting for certain conditions to be met is a crucial aspect of ensuring smooth and reliable test execution. In this post, we'll walk through a powerful utility called GenericWait that simplifies conditional waiting in your automation scripts. We'll break down how it works, its advantages, and the best practices to use it efficiently.

Why Waiting in Automation is Important

In test automation, especially when dealing with UI, API, or database validations, elements or conditions may not be immediately available for interaction. Relying on static Thread.sleep() can cause unnecessary delays or inconsistent results. Smart waiting mechanisms, like the one we'll build, ensure that the automation waits just enough time for a condition to become true without unnecessary waits or errors.

Understanding the GenericWait Utility

The GenericWait utility is a flexible solution that uses Java's Callable<T> interface to perform repeated checks (polling) of a condition until it returns a valid result or a timeout occurs. Here's how it works:

Callable Interface

The Callable<T> is a functional interface that represents a task which can return a result or throw an exception. It's similar to Runnable, but it can return a result and handle exceptions. Its signature is T call() throws Exception. It's ideal for tasks where you need to check a condition repeatedly until it's met or a timeout occurs.

Polling for Conditions

With GenericWait, we execute a callable task (your condition) repeatedly within a given timeout period, pausing between each execution. If the condition is met, the result is returned. If the timeout is exceeded without success, a TimeoutException is thrown.

Step-by-Step Breakdown of GenericWait Code

Here’s the complete code for the GenericWait utility class:

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;

public class GenericWait {

    /**
     * Waits until the given condition returns a non-null and non-false value.
     *
     * @param <T>           the return type
     * @param condition     the condition to evaluate
     * @param timeout       max time to wait
     * @param pollInterval  polling interval
     * @return the result of the condition when it succeeds
     * @throws TimeoutException if timeout is reached
     */
    public static <T> T waitUntil(Callable<T> condition, Duration timeout, Duration pollInterval) throws TimeoutException {
        Instant end = Instant.now().plus(timeout);

        while (Instant.now().isBefore(end)) {
            try {
                T result = condition.call();
                if (result instanceof Boolean && !(Boolean) result) {
                    // Boolean false, keep waiting
                } else if (result != null) {
                    return result;
                }

            } catch (Exception ignored) {
                // Can log here if needed
            }

            try {
                Thread.sleep(pollInterval.toMillis());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Thread was interrupted during wait", e);
            }
        }

        throw new TimeoutException("Timeout reached while waiting for condition");
    }
}

Key Points Explained

1. waitUntil() Method: This method takes in a Callable<T> condition, a timeout, and a pollInterval. The idea is to keep calling the condition.call() at fixed intervals (polling) until it returns a valid value or the timeout is exceeded.
2. Timeout Handling: We calculate the end time by adding the timeout to the current time, and check if the current time has exceeded this end time.
3. Polling and Sleeping: After each condition check, we pause the execution for the duration of pollInterval. This prevents excessive CPU usage by continuously evaluating the condition.
4. Error Handling: We catch all exceptions that might be thrown during condition.call() and ignore them. Optionally, you can log these exceptions for debugging purposes.
5. Returning Results: If the condition is met (i.e., the result is not null or false for Boolean conditions), we return the result immediately. If the condition is not met, the process continues until the timeout is reached.

Example 1: Wait for a Web Element

This example demonstrates how to wait for a WebElement to become visible and enabled on the page. We use the waitUntil method and pass a lambda expression that checks if the element is displayed and enabled.

public static WebElement waitForElement(WebDriver driver, By locator, long timeoutInSeconds, long pollingIntervalInMillis) {
    return waitUntil(driver, () -> {
        try {
            WebElement element = driver.findElement(locator);
            return (element.isDisplayed() && element.isEnabled()) ? element : null;
        } catch (NoSuchElementException | StaleElementReferenceException e) {
            return null;  // Return null if element is not found or stale
        }
    }, timeoutInSeconds, pollingIntervalInMillis);
}

Usage:

WebElement submitButton = waitForElement(driver, By.id("submitButton"), 30, 500);  // 500ms polling interval

Explanation:

  • This method uses the waitUntil utility to repeatedly check for the presence of the element defined by locator.

  • The condition checks whether the element is both displayed and enabled.

  • If the element is not found, or if the element is stale, it returns null, causing a retry until the timeout is exceeded.

🔁 Retry Behavior:

  • Callable tries to locate the element using driver.findElement(locator).

  • Checks if the element is visible and enabled.

  • If the condition fails, it returns null, and the waitUntil method retries after the polling interval.

Advantages:

  • Non-crashing: Exceptions are caught and retried.

  • Reusable: Can be used for any condition.

  • Flexible: Easy to customize polling interval, timeout, and behavior.

Example 2: Multiple Conditions with WebElement

This example demonstrates a more complex scenario where you need to check for multiple conditions before considering an element as valid. For example, you may want to ensure the element is both visible and has a specific attribute.

Method to Wait for Element with Multiple Conditions:

public static WebElement waitForElementWithMultipleConditions(WebDriver driver, By locator, String expectedText, long timeoutInSeconds, long pollingIntervalInMillis) {
    return waitUntil(driver, () -> {
        try {
            WebElement element = driver.findElement(locator);
            if (element.isDisplayed() && element.isEnabled() && element.getText().equals(expectedText)) {
                return element;
            }
            return null;
        } catch (NoSuchElementException | StaleElementReferenceException e) {
            return null;  // Return null if element is not found or stale
        }
    }, timeoutInSeconds, pollingIntervalInMillis);
}

Usage:

WebElement submitButton = waitForElementWithMultipleConditions(driver, By.id("submitButton"), "Submit", 30, 500);

Explanation:

  • This method waits for the element to be displayed, enabled, and to contain the expected text.

  • If the element doesn’t meet all conditions, it returns null and retries the operation until the timeout is reached.

Example 3: Wait for a File Download (Non-Selenium Scenario)

In real-world automation, waiting for a file to download is a common use case that requires a dynamic wait mechanism. Here, we’ll use the Callable interface to wait for a file to appear in the download folder.

Method to Wait for File Download:

import java.io.File;

public static boolean waitForFileDownload(String downloadDir, String fileName, long timeoutInSeconds, long pollingIntervalInMillis) {
    return waitUntil(null, () -> {
        File file = new File(downloadDir + "/" + fileName);
        return file.exists() && file.isFile();
    }, timeoutInSeconds, pollingIntervalInMillis);
}

Usage:

boolean isFileDownloaded = waitForFileDownload("/path/to/download", "testFile.pdf", 30, 500);

Explanation:

  • This method waits for the specified file to appear in the download directory.

  • It checks if the file exists and if it’s a regular file.

  • If the file is not found, it retries until the timeout is exceeded.

Example 4: Wait for Network Condition (Non-Selenium Scenario)

For web scraping or API testing, you may need to wait for network conditions to stabilize before proceeding. For instance, waiting for an API response or checking network connectivity status.

Method to Wait for API Response:

public static boolean waitForApiResponse(String apiUrl, long timeoutInSeconds, long pollingIntervalInMillis) {
    return waitUntil(null, () -> {
        int statusCode = getApiResponseStatusCode(apiUrl);  // Hypothetical method to get API status
        return statusCode == 200;
    }, timeoutInSeconds, pollingIntervalInMillis);
}

Usage:

boolean isApiAvailable = waitForApiResponse("https://api.example.com", 30, 500);

Explanation:

  • This method waits for a successful API response (status code 200).

  • It checks the status code periodically until the timeout is exceeded.

Conclusion

By using functional interfaces like Callable and customizing the wait mechanism with dynamic polling intervals, you can create more flexible, reusable, and robust waiting strategies for your test automation.

This approach not only works for Selenium but can be extended to handle various conditions, including network conditions, file downloads, and API responses. The retry mechanism ensures that your tests are resilient to temporary failures, and the dynamic wait intervals give you control over the polling frequency.

Benefits of this approach:

  • Flexibility: Easy to adjust for different conditions.

  • Control: You control polling intervals and timeouts.

  • Reusability: This pattern is adaptable to different kinds of conditions beyond Selenium elements.

By incorporating these techniques into your automation framework, you can significantly improve the stability and maintainability of your test automation suite.

0
Subscribe to my newsletter

Read articles from Raviteja Gutti directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Raviteja Gutti
Raviteja Gutti