Previous: Selectors and Element Interactions Next: Advanced WebDriverIO Features

Module 3: Asynchronous Testing with WebDriverIO

Overview

This module explores asynchronous testing in WebDriverIO, a critical concept for creating reliable and efficient test automation. You'll learn how WebDriverIO handles asynchronous operations, how to work with promises and async/await, and how to implement effective asynchronous testing patterns.

Learning Objectives

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

  1. Understand the asynchronous nature of WebDriverIO
  2. Implement tests using promises and async/await syntax
  3. Handle race conditions and timing issues in tests
  4. Implement synchronous-like test flows with asynchronous code
  5. Debug asynchronous test failures effectively
  6. Create custom asynchronous utilities for testing

3.1 Understanding Asynchronous Testing

The Asynchronous Nature of Web Testing

Web testing is inherently asynchronous due to several factors:

  1. Browser Interactions: Commands to the browser don't execute instantly
  2. Network Requests: API calls, page loads, and resource fetching take time
  3. Animations and Transitions: UI changes may involve animations
  4. Dynamic Content: Content may load or change based on user actions
  5. Backend Processing: Server-side operations affect what's displayed

Synchronous vs. Asynchronous Code

Understanding the difference between synchronous and asynchronous code:

// Synchronous code (executes line by line)
const result = 1 + 1;
console.log(result);
console.log('Done');

// Asynchronous code (doesn't wait for completion)
setTimeout(() => {
    console.log('This executes later');
}, 1000);
console.log('This executes first');

3.2 WebDriverIO's Asynchronous Architecture

How WebDriverIO Handles Asynchronous Operations

WebDriverIO uses a client-server architecture where:

  1. Test script (client) sends commands to the WebDriver server
  2. WebDriver server executes commands in the browser
  3. Results are returned to the test script

All WebDriverIO commands return promises, making them asynchronous by nature.

WebDriverIO v7+ and Async/Await

Since version 7, WebDriverIO has fully embraced the async/await pattern:

// WebDriverIO v7+ with async/await
it('should demonstrate async/await', async () => {
    await browser.url('https://webdriver.io');
    const title = await browser.getTitle();
    expect(title).toContain('WebdriverIO');
});

3.3 Working with Promises in WebDriverIO

Promise Basics

A Promise in JavaScript represents a value that might not be available yet:

// Creating a promise
const myPromise = new Promise((resolve, reject) => {
    // Asynchronous operation
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('Operation completed successfully');
        } else {
            reject(new Error('Operation failed'));
        }
    }, 1000);
});

// Consuming a promise
myPromise
    .then(result => console.log(result))
    .catch(error => console.error(error));

Promise Chaining

Promises can be chained to create a sequence of asynchronous operations:

// Promise chaining
browser.url('https://webdriver.io')
    .then(() => browser.getTitle())
    .then(title => {
        expect(title).toContain('WebdriverIO');
        return browser.getUrl();
    })
    .then(url => {
        expect(url).toContain('webdriver.io');
    })
    .catch(error => {
        console.error('Test failed:', error);
    });

3.4 Async/Await in WebDriverIO

Basic Async/Await Syntax

The async/await syntax makes asynchronous code look synchronous:

// Basic async/await syntax
it('should use async/await', async () => {
    // 'await' can only be used in 'async' functions
    await browser.url('https://webdriver.io');

    const title = await browser.getTitle();
    expect(title).toContain('WebdriverIO');

    const isDisplayed = await $('#header').isDisplayed();
    expect(isDisplayed).toBe(true);
});

Error Handling with Try/Catch

Use try/catch blocks to handle errors in async/await code:

// Error handling with try/catch
it('should handle errors with try/catch', async () => {
    try {
        await browser.url('https://webdriver.io');
        await $('#non-existent-element').click();
    } catch (error) {
        console.error('Test failed:', error);
        // You can also take screenshots, log additional info, etc.
        await browser.saveScreenshot('./error-screenshot.png');
        throw error; // Re-throw to fail the test
    }
});

3.5 Handling Race Conditions and Timing Issues

Waiting Strategies

Implement effective waiting strategies to handle timing issues:

// Explicit waits
it('should use explicit waits', async () => {
    await browser.url('/dynamic-page');

    // Wait for specific element
    await $('#dynamicElement').waitForDisplayed({ timeout: 5000 });

    // Wait for element to contain text
    await browser.waitUntil(async () => {
        return (await $('#status').getText()) === 'Ready';
    }, {
        timeout: 10000,
        timeoutMsg: 'Expected status to be Ready after 10s'
    });

    // Wait for URL to change
    await browser.waitUntil(async () => {
        return (await browser.getUrl()).includes('/ready');
    });
});

3.6 Advanced Asynchronous Patterns

Custom Async Utilities

Create reusable async utilities for common testing patterns:

// utils/async-helpers.js
class AsyncHelpers {
    /**
     * Waits for a condition to be true with polling
     */
    static async waitForCondition(condition, options = {}) {
        const {
            timeout = 10000,
            interval = 500,
            timeoutMsg = 'Condition not met within timeout'
        } = options;

        await browser.waitUntil(condition, { timeout, interval, timeoutMsg });
    }

    /**
     * Retries an operation until it succeeds or reaches max retries
     */
    static async retry(operation, options = {}) {
        const {
            maxRetries = 3,
            retryInterval = 500,
            retryCondition = (error) => true
        } = options;

        let lastError;

        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return await operation();
            } catch (error) {
                lastError = error;

                if (attempt < maxRetries && retryCondition(error)) {
                    await browser.pause(retryInterval);
                } else {
                    throw error;
                }
            }
        }
    }
}

module.exports = AsyncHelpers;

3.7 Debugging Asynchronous Tests

Debugging Techniques

Techniques for debugging asynchronous tests:

// Using browser.debug() to pause execution
it('should debug async test', async () => {
    await browser.url('/complex-page');

    // Pause execution for debugging
    await browser.debug();

    // Continue execution after manual debugging
    await $('#button').click();
});

// Using console.log with async values
it('should log async values', async () => {
    await browser.url('/page');

    const title = await browser.getTitle();
    console.log('Page title:', title);

    const element = await $('#element');
    const isDisplayed = await element.isDisplayed();
    console.log('Element displayed:', isDisplayed);
});

Practical Exercises

Exercise 1: Asynchronous Test Basics

  1. Create a test file that navigates to https://the-internet.herokuapp.com/dynamic_loading/1
  2. Implement a test that clicks the "Start" button and waits for the loading indicator to disappear
  3. Verify the hidden element is displayed
  4. Implement the same test using both Promise chains (.then()) and async/await
  5. Compare the readability and maintainability of both approaches

Exercise 2: Handling Race Conditions

  1. Create a test file that navigates to https://the-internet.herokuapp.com/dynamic_controls
  2. Implement proper waiting strategies to handle the dynamic nature of the page
  3. Test removing and adding checkboxes with proper synchronization

Exercise 3: Custom Async Utilities

  1. Create an async-helpers.js file with custom utility functions
  2. Implement waitForTextChange, retryClick, and waitForUrlToContain functions
  3. Use these utilities in your tests to make them more robust and readable

Summary

In this module, you learned about asynchronous testing with WebDriverIO, including:

Previous: Selectors and Element Interactions Next: Advanced WebDriverIO Features