Previous: Page Object Model Next: Debugging and Troubleshooting

Module 7: WebDriverIO with Cucumber Integration

Learning Objectives

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

Introduction to Behavior-Driven Development (BDD)

What is BDD?

Behavior-Driven Development (BDD) is a software development approach that encourages collaboration between developers, QA, and business stakeholders. It focuses on defining the behavior of an application through examples in plain language.

Benefits of BDD with WebDriverIO

Setting Up Cucumber with WebDriverIO

Installation

Install the necessary packages for Cucumber integration:

# Install Cucumber and WebDriverIO Cucumber service
npm install --save-dev @cucumber/cucumber @wdio/cucumber-framework
npm install --save-dev @wdio/spec-reporter @wdio/allure-reporter

# Install additional utilities
npm install --save-dev cucumber-html-reporter

WebDriverIO Configuration

Configure WebDriverIO to use the Cucumber framework:

// wdio.conf.js
exports.config = {
    // Test framework
    framework: 'cucumber',
    
    // Cucumber options
    cucumberOpts: {
        // Require files before executing features
        require: ['./features/step-definitions/**/*.js'],
        
        // Timeout for step definitions
        timeout: 60000,
        
        // Format options
        format: ['pretty'],
        
        // Fail fast
        failFast: false,
        
        // Ignore undefined definitions
        ignoreUndefinedDefinitions: false,
        
        // Tags to run
        tagExpression: 'not @skip',
        
        // Retry failed scenarios
        retry: 1
    },
    
    // Spec patterns for feature files
    specs: [
        './features/**/*.feature'
    ],
    
    // Reporters
    reporters: [
        'spec',
        ['allure', {
            outputDir: 'allure-results',
            disableWebdriverStepsReporting: true,
            disableWebdriverScreenshotsReporting: false
        }]
    ],
    
    // Hooks
    beforeScenario: function (world, context) {
        // Set up before each scenario
        console.log(`Starting scenario: ${context.pickle.name}`);
    },
    
    afterScenario: function (world, result, context) {
        // Clean up after each scenario
        if (result.status === 'FAILED') {
            // Take screenshot on failure
            const screenshot = browser.takeScreenshot();
            browser.saveScreenshot(`./screenshots/failed-${Date.now()}.png`);
        }
    }
};

Writing Feature Files

Gherkin Syntax

Gherkin is the language used to write feature files. It uses keywords like Given, When, Then, And, But:

# features/login.feature
Feature: User Authentication
  As a user
  I want to be able to log in to the application
  So that I can access my account

  Background:
    Given I am on the login page

  @smoke @authentication
  Scenario: Successful login with valid credentials
    When I enter valid username "testuser@example.com"
    And I enter valid password "password123"
    And I click the login button
    Then I should be redirected to the dashboard
    And I should see a welcome message

  @authentication @negative
  Scenario: Failed login with invalid credentials
    When I enter invalid username "invalid@example.com"
    And I enter invalid password "wrongpassword"
    And I click the login button
    Then I should see an error message "Invalid credentials"
    And I should remain on the login page

  @authentication
  Scenario Outline: Login with different user types
    When I enter username "<username>"
    And I enter password "<password>"
    And I click the login button
    Then I should see "<expected_result>"

    Examples:
      | username           | password    | expected_result |
      | admin@example.com  | admin123    | Admin Dashboard |
      | user@example.com   | user123     | User Dashboard  |
      | guest@example.com  | guest123    | Guest Dashboard |

Feature File Organization

Organize your feature files in a logical structure:

features/
├── authentication/
│   ├── login.feature
│   ├── logout.feature
│   └── password-reset.feature
├── e-commerce/
│   ├── product-search.feature
│   ├── shopping-cart.feature
│   └── checkout.feature
├── user-management/
│   ├── user-registration.feature
│   └── profile-management.feature
└── step-definitions/
    ├── authentication/
    │   └── login-steps.js
    ├── e-commerce/
    │   └── shopping-steps.js
    └── common/
        └── common-steps.js

Implementing Step Definitions

Basic Step Definitions

Implement step definitions that map Gherkin steps to WebDriverIO actions:

// features/step-definitions/authentication/login-steps.js
const { Given, When, Then } = require('@cucumber/cucumber');

Given(/^I am on the login page$/, async () => {
    await browser.url('/login');
    await expect(browser).toHaveTitle('Login - My Application');
});

When(/^I enter valid username "([^"]*)"$/, async (username) => {
    const usernameField = await $('[data-testid="username"]');
    await usernameField.setValue(username);
});

When(/^I enter valid password "([^"]*)"$/, async (password) => {
    const passwordField = await $('[data-testid="password"]');
    await passwordField.setValue(password);
});

When(/^I enter invalid username "([^"]*)"$/, async (username) => {
    const usernameField = await $('[data-testid="username"]');
    await usernameField.setValue(username);
});

When(/^I enter invalid password "([^"]*)"$/, async (password) => {
    const passwordField = await $('[data-testid="password"]');
    await passwordField.setValue(password);
});

When(/^I click the login button$/, async () => {
    const loginButton = await $('[data-testid="login-btn"]');
    await loginButton.click();
});

Then(/^I should be redirected to the dashboard$/, async () => {
    await browser.waitUntil(
        async () => (await browser.getUrl()).includes('/dashboard'),
        {
            timeout: 5000,
            timeoutMsg: 'Expected to be redirected to dashboard'
        }
    );
});

Then(/^I should see a welcome message$/, async () => {
    const welcomeMessage = await $('[data-testid="welcome-message"]');
    await expect(welcomeMessage).toBeDisplayed();
    await expect(welcomeMessage).toHaveTextContaining('Welcome');
});

Then(/^I should see an error message "([^"]*)"$/, async (expectedMessage) => {
    const errorMessage = await $('[data-testid="error-message"]');
    await expect(errorMessage).toBeDisplayed();
    await expect(errorMessage).toHaveText(expectedMessage);
});

Then(/^I should remain on the login page$/, async () => {
    const currentUrl = await browser.getUrl();
    expect(currentUrl).toContain('/login');
});

Parameterized Steps

Create flexible step definitions that accept parameters:

// features/step-definitions/common/common-steps.js
const { Given, When, Then } = require('@cucumber/cucumber');

// Generic navigation step
Given(/^I navigate to "([^"]*)"$/, async (url) => {
    await browser.url(url);
});

// Generic form field interaction
When(/^I enter "([^"]*)" in the "([^"]*)" field$/, async (value, fieldName) => {
    const field = await $(`[data-testid="${fieldName}"]`);
    await field.setValue(value);
});

// Generic button click
When(/^I click the "([^"]*)" button$/, async (buttonName) => {
    const button = await $(`[data-testid="${buttonName}-btn"]`);
    await button.click();
});

// Generic text verification
Then(/^I should see "([^"]*)" on the page$/, async (expectedText) => {
    const pageText = await browser.getText('body');
    expect(pageText).toContain(expectedText);
});

// Generic element visibility check
Then(/^the "([^"]*)" element should be visible$/, async (elementName) => {
    const element = await $(`[data-testid="${elementName}"]`);
    await expect(element).toBeDisplayed();
});

// Generic element text verification
Then(/^the "([^"]*)" element should contain "([^"]*)"$/, async (elementName, expectedText) => {
    const element = await $(`[data-testid="${elementName}"]`);
    await expect(element).toHaveTextContaining(expectedText);
});

Advanced Cucumber Features

Data Tables

Use data tables for complex data input:

# Feature file with data table
Scenario: User registration with complete profile
  Given I am on the registration page
  When I fill in the registration form with the following details:
    | Field           | Value                |
    | First Name      | John                 |
    | Last Name       | Doe                  |
    | Email           | john.doe@example.com |
    | Phone           | +1-555-123-4567      |
    | Date of Birth   | 01/15/1990           |
    | Country         | United States        |
  And I click the register button
  Then I should see a success message
// Step definition for data table
When(/^I fill in the registration form with the following details:$/, async (dataTable) => {
    const data = dataTable.rowsHash();
    
    for (const [field, value] of Object.entries(data)) {
        const fieldSelector = getFieldSelector(field);
        const fieldElement = await $(fieldSelector);
        await fieldElement.setValue(value);
    }
});

function getFieldSelector(fieldName) {
    const fieldMap = {
        'First Name': '[data-testid="first-name"]',
        'Last Name': '[data-testid="last-name"]',
        'Email': '[data-testid="email"]',
        'Phone': '[data-testid="phone"]',
        'Date of Birth': '[data-testid="dob"]',
        'Country': '[data-testid="country"]'
    };
    
    return fieldMap[fieldName] || `[data-testid="${fieldName.toLowerCase().replace(/\s+/g, '-')}"]`;
}

Hooks

Implement hooks for setup and teardown operations:

// features/step-definitions/hooks.js
const { Before, After, BeforeAll, AfterAll } = require('@cucumber/cucumber');

BeforeAll(async () => {
    console.log('Setting up test environment...');
    // Global setup operations
});

Before(async (scenario) => {
    console.log(`Starting scenario: ${scenario.pickle.name}`);
    
    // Clear browser storage before each scenario
    await browser.execute(() => {
        localStorage.clear();
        sessionStorage.clear();
    });
    
    // Set browser window size
    await browser.setWindowSize(1920, 1080);
});

Before({ tags: '@database' }, async () => {
    // Setup specific to scenarios tagged with @database
    console.log('Setting up database for scenario...');
    // Database setup operations
});

After(async (scenario) => {
    if (scenario.result.status === 'FAILED') {
        // Take screenshot on failure
        const screenshot = await browser.takeScreenshot();
        await browser.saveScreenshot(`./screenshots/failed-${scenario.pickle.name}-${Date.now()}.png`);
        
        // Attach screenshot to Allure report
        if (typeof allure !== 'undefined') {
            allure.addAttachment('Screenshot', screenshot, 'image/png');
        }
    }
});

AfterAll(async () => {
    console.log('Cleaning up test environment...');
    // Global cleanup operations
});

Page Object Integration

Using Page Objects with Cucumber

Integrate Page Object Model with Cucumber step definitions:

// pages/LoginPage.js
class LoginPage {
    get usernameField() { return $('[data-testid="username"]'); }
    get passwordField() { return $('[data-testid="password"]'); }
    get loginButton() { return $('[data-testid="login-btn"]'); }
    get errorMessage() { return $('[data-testid="error-message"]'); }

    async open() {
        await browser.url('/login');
    }

    async login(username, password) {
        await this.usernameField.setValue(username);
        await this.passwordField.setValue(password);
        await this.loginButton.click();
    }

    async getErrorMessage() {
        await this.errorMessage.waitForDisplayed();
        return await this.errorMessage.getText();
    }
}

module.exports = new LoginPage();
// features/step-definitions/authentication/login-steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const LoginPage = require('../../../pages/LoginPage');
const DashboardPage = require('../../../pages/DashboardPage');

Given(/^I am on the login page$/, async () => {
    await LoginPage.open();
});

When(/^I login with username "([^"]*)" and password "([^"]*)"$/, async (username, password) => {
    await LoginPage.login(username, password);
});

Then(/^I should be on the dashboard$/, async () => {
    await expect(DashboardPage.welcomeMessage).toBeDisplayed();
});

Then(/^I should see login error "([^"]*)"$/, async (expectedError) => {
    const actualError = await LoginPage.getErrorMessage();
    expect(actualError).toBe(expectedError);
});

Reporting

Cucumber HTML Reports

Generate comprehensive HTML reports for Cucumber tests:

// generate-report.js
const reporter = require('cucumber-html-reporter');

const options = {
    theme: 'bootstrap',
    jsonFile: 'reports/cucumber-report.json',
    output: 'reports/cucumber-report.html',
    reportSuiteAsScenarios: true,
    scenarioTimestamp: true,
    launchReport: true,
    metadata: {
        "App Version": "1.0.0",
        "Test Environment": "STAGING",
        "Browser": "Chrome 91",
        "Platform": "Windows 10",
        "Parallel": "Scenarios",
        "Executed": "Remote"
    }
};

reporter.generate(options);

Allure Integration

Enhance Cucumber reports with Allure:

// features/step-definitions/allure-steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const allure = require('@wdio/allure-reporter').default;

Given(/^I am on the login page$/, async () => {
    allure.addStep('Navigate to login page', async () => {
        await browser.url('/login');
    });
    
    allure.addStep('Verify login page is loaded', async () => {
        await expect(browser).toHaveTitle('Login - My Application');
    });
});

When(/^I perform login with "([^"]*)" and "([^"]*)"$/, async (username, password) => {
    allure.addStep(`Enter username: ${username}`, async () => {
        await $('[data-testid="username"]').setValue(username);
    });
    
    allure.addStep('Enter password', async () => {
        await $('[data-testid="password"]').setValue(password);
    });
    
    allure.addStep('Click login button', async () => {
        await $('[data-testid="login-btn"]').click();
    });
});

Best Practices

Writing Good Scenarios

Step Definition Best Practices

Practical Exercise

Exercise: E-commerce BDD Test Suite

Objective: Create a comprehensive BDD test suite for an e-commerce application using Cucumber and WebDriverIO.

Requirements:

  1. Create feature files for:
    • User registration and login
    • Product search and filtering
    • Shopping cart operations
    • Checkout process
  2. Implement corresponding step definitions
  3. Use data tables and scenario outlines
  4. Integrate with Page Object Model
  5. Set up proper hooks for test setup/teardown
  6. Generate HTML and Allure reports

Deliverables:

Summary

In this module, you've learned how to integrate Cucumber with WebDriverIO for behavior-driven development. You now understand how to:

BDD with Cucumber provides a powerful way to create tests that serve as living documentation and facilitate collaboration between technical and non-technical team members.

Previous: Page Object Model Next: Debugging and Troubleshooting