By the end of this module, you will be able to:
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.
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
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`);
}
}
};
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 |
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
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');
});
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);
});
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, '-')}"]`;
}
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
});
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);
});
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);
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();
});
});
Objective: Create a comprehensive BDD test suite for an e-commerce application using Cucumber and WebDriverIO.
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.