Introduction to Cucumber BDD
Cucumber has revolutionized software testing by bridging the gap between technical and non-technical stakeholders. As a behavior-driven development (BDD) (as discussed in BDD: From Requirements to Automation) framework, Cucumber enables teams to write test scenarios in plain language that both business analysts and developers can understand, fostering collaboration and ensuring that software meets actual business requirements.
This comprehensive guide explores Cucumber automation (as discussed in Gauge Framework Guide: Language-Independent BDD Alternative to Cucumber) from fundamentals to advanced techniques, covering Gherkin syntax, feature file organization, step definition patterns, data-driven testing, hooks, and comprehensive reporting strategies.
Understanding Gherkin Syntax
Gherkin is Cucumber’s business-readable, domain-specific language for describing software behavior. It uses a structured format with specific keywords to create executable specifications.
Core Gherkin Keywords
Feature: User Authentication
As a registered user
I want to log into the system
So that I can access my account
Background:
Given the application is running
And the database is initialized
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter username "john.doe@example.com"
And I enter password "SecurePass123"
And I click the login button
Then I should be redirected to the dashboard
And I should see a welcome message "Welcome, John"
Scenario: Failed login with invalid password
Given I am on the login page
When I enter username "john.doe@example.com"
And I enter 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
Gherkin Keyword Breakdown
Keyword | Purpose | Example |
---|---|---|
Feature | Groups related scenarios | Feature: User Registration |
Background | Runs before each scenario | Background: Given logged in user |
Scenario | Individual test case | Scenario: Add item to cart |
Given | Preconditions/setup | Given I am on homepage |
When | Action/event | When I click "Buy Now" |
Then | Expected outcome | Then I see confirmation |
And/But | Connects steps | And I receive email |
Feature File Organization
Effective feature file structure is crucial for maintainability and clarity.
Feature File Best Practices
# features/e-commerce/shopping_cart.feature
@shopping @critical
Feature: Shopping Cart Management
As an online shopper
I want to manage items in my shopping cart
So that I can purchase products
Background:
Given I am logged in as "standard_user"
And I am on the products page
@smoke @cart-add
Scenario: Add single product to cart
When I add "Laptop Computer" to cart
Then the cart count should be 1
And the cart total should be "$999.99"
@cart-add
Scenario: Add multiple quantities of same product
When I add 3 units of "USB Cable" to cart
Then the cart count should be 3
And the cart total should be "$29.97"
@cart-remove
Scenario: Remove product from cart
Given I have "Wireless Mouse" in my cart
When I remove "Wireless Mouse" from cart
Then the cart count should be 0
And the cart should be empty
@cart-update
Scenario: Update product quantity in cart
Given I have 2 units of "Keyboard" in my cart
When I update "Keyboard" quantity to 5
Then the cart count should be 5
And the cart total should be "$249.95"
Scenario Outline for Data-Driven Testing
# features/authentication/login_validation.feature
Feature: Login Input Validation
@parametrized @validation
Scenario Outline: Validate login with different credentials
Given I am on the login page
When I enter username "<username>"
And I enter password "<password>"
And I click the login button
Then I should see "<result>"
Examples:
| username | password | result |
| valid@example.com | ValidPass123 | Welcome to Dashboard |
| invalid@example.com | anything | User not found |
| valid@example.com | wrongpass | Invalid credentials |
| @invalid.com | ValidPass123 | Invalid email format |
| valid@example.com | 123 | Password too short |
@edge-cases
Examples: Edge Cases
| username | password | result |
| | ValidPass123 | Username required |
| valid@example.com | | Password required |
| | | Username and password required |
Step Definitions: Connecting Gherkin to Code
Step definitions translate Gherkin steps into executable code.
Java Step Definitions
// src/test/java/steps/AuthenticationSteps.java
package steps;
import io.cucumber.java.en.*;
import pages.LoginPage;
import pages.DashboardPage;
import static org.junit.Assert.*;
public class AuthenticationSteps {
private LoginPage loginPage;
private DashboardPage dashboardPage;
private String actualMessage;
@Given("I am on the login page")
public void navigateToLoginPage() {
loginPage = new LoginPage();
loginPage.navigate();
}
@When("I enter username {string}")
public void enterUsername(String username) {
loginPage.enterUsername(username);
}
@When("I enter password {string}")
public void enterPassword(String password) {
loginPage.enterPassword(password);
}
@When("I click the login button")
public void clickLoginButton() {
dashboardPage = loginPage.clickLogin();
}
@Then("I should be redirected to the dashboard")
public void verifyDashboardRedirect() {
assertTrue("Not on dashboard",
dashboardPage.isDisplayed());
}
@Then("I should see a welcome message {string}")
public void verifyWelcomeMessage(String expectedMessage) {
String actualMessage = dashboardPage.getWelcomeMessage();
assertEquals(expectedMessage, actualMessage);
}
@Then("I should see an error message {string}")
public void verifyErrorMessage(String expectedError) {
String actualError = loginPage.getErrorMessage();
assertEquals(expectedError, actualError);
}
}
JavaScript/TypeScript Step Definitions
// features/step_definitions/authentication.steps.ts
import { Given, When, Then } from '@cucumber/cucumber' (as discussed in [Serenity BDD Integration: Living Documentation and Advanced Test Reporting](/blog/serenity-bdd-integration));
import { expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
Given('I am on the login page', async function() {
loginPage = new LoginPage(this.page);
await loginPage.navigate();
});
When('I enter username {string}', async function(username: string) {
await loginPage.enterUsername(username);
});
When('I enter password {string}', async function(password: string) {
await loginPage.enterPassword(password);
});
When('I click the login button', async function() {
await loginPage.clickLogin();
dashboardPage = new DashboardPage(this.page);
});
Then('I should be redirected to the dashboard', async function() {
await expect(dashboardPage.container).toBeVisible();
});
Then('I should see a welcome message {string}',
async function(expectedMessage: string) {
const actualMessage = await dashboardPage.getWelcomeMessage();
expect(actualMessage).toBe(expectedMessage);
});
Python Step Definitions
# features/steps/authentication_steps.py
from behave import given, when, then
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
@given('I am on the login page')
def navigate_to_login(context):
context.login_page = LoginPage(context.driver)
context.login_page.navigate()
@when('I enter username "{username}"')
def enter_username(context, username):
context.login_page.enter_username(username)
@when('I enter password "{password}"')
def enter_password(context, password):
context.login_page.enter_password(password)
@when('I click the login button')
def click_login(context):
context.login_page.click_login()
context.dashboard_page = DashboardPage(context.driver)
@then('I should be redirected to the dashboard')
def verify_dashboard_redirect(context):
assert context.dashboard_page.is_displayed(), \
"Dashboard not displayed"
@then('I should see a welcome message "{message}"')
def verify_welcome_message(context, message):
actual = context.dashboard_page.get_welcome_message()
assert actual == message, \
f"Expected '{message}', got '{actual}'"
Data Tables: Advanced Data Structures
Data tables enable complex data passing to step definitions.
Using Data Tables in Gherkin
Feature: User Registration
Scenario: Register new user with complete profile
Given I am on the registration page
When I fill in the registration form:
| Field | Value |
| First Name | John |
| Last Name | Doe |
| Email | john.doe@example.com |
| Phone | +1-555-0123 |
| Password | SecurePass123! |
| Confirm Pass | SecurePass123! |
And I accept the terms and conditions
And I click the register button
Then I should see a confirmation message
And I should receive a welcome email
Scenario: Create multiple products
Given I am logged in as admin
When I create the following products:
| Name | Category | Price | Stock |
| Laptop Pro | Electronics | 999.99 | 50 |
| USB-C Cable | Accessories | 9.99 | 200 |
| Wireless Mouse| Accessories | 29.99 | 100 |
Then all products should be visible in catalog
And the total inventory value should be "$53,998.00"
Processing Data Tables in Step Definitions
// Java - Processing data tables
import io.cucumber.datatable.DataTable;
import java.util.List;
import java.util.Map;
@When("I fill in the registration form:")
public void fillRegistrationForm(DataTable dataTable) {
Map<String, String> data = dataTable.asMap(String.class, String.class);
registrationPage.enterFirstName(data.get("First Name"));
registrationPage.enterLastName(data.get("Last Name"));
registrationPage.enterEmail(data.get("Email"));
registrationPage.enterPhone(data.get("Phone"));
registrationPage.enterPassword(data.get("Password"));
registrationPage.confirmPassword(data.get("Confirm Pass"));
}
@When("I create the following products:")
public void createMultipleProducts(DataTable dataTable) {
List<Map<String, String>> products = dataTable.asMaps();
for (Map<String, String> product : products) {
productService.create(
product.get("Name"),
product.get("Category"),
Double.parseDouble(product.get("Price")),
Integer.parseInt(product.get("Stock"))
);
}
}
// TypeScript - Processing data tables
import { DataTable } from '@cucumber/cucumber';
When('I fill in the registration form:',
async function(dataTable: DataTable) {
const data = dataTable.rowsHash();
await this.registrationPage.enterFirstName(data['First Name']);
await this.registrationPage.enterLastName(data['Last Name']);
await this.registrationPage.enterEmail(data['Email']);
await this.registrationPage.enterPhone(data['Phone']);
await this.registrationPage.enterPassword(data['Password']);
await this.registrationPage.confirmPassword(data['Confirm Pass']);
});
When('I create the following products:',
async function(dataTable: DataTable) {
const products = dataTable.hashes();
for (const product of products) {
await this.productService.create({
name: product.Name,
category: product.Category,
price: parseFloat(product.Price),
stock: parseInt(product.Stock)
});
}
});
Hooks: Setup and Teardown
Hooks provide lifecycle management for scenarios and steps.
Java Hooks Implementation
// src/test/java/hooks/Hooks.java
package hooks;
import io.cucumber.java.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import utils.ScreenshotUtils;
import utils.DatabaseUtils;
public class Hooks {
private WebDriver driver;
private DatabaseUtils database;
@BeforeAll
public static void beforeAll() {
// Runs once before all scenarios
System.out.println("Starting test suite execution");
DatabaseUtils.initializeTestDatabase();
}
@Before
public void beforeScenario(Scenario scenario) {
// Runs before each scenario
System.out.println("Starting: " + scenario.getName());
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@Before("@database")
public void beforeDatabaseScenario() {
// Runs only for scenarios tagged with @database
database = new DatabaseUtils();
database.connect();
}
@After
public void afterScenario(Scenario scenario) {
// Runs after each scenario
if (scenario.isFailed()) {
byte[] screenshot = ScreenshotUtils.takeScreenshot(driver);
scenario.attach(screenshot, "image/png", "failure_screenshot");
}
if (driver != null) {
driver.quit();
}
}
@After("@database")
public void afterDatabaseScenario() {
// Cleanup database connections
if (database != null) {
database.disconnect();
}
}
@AfterStep
public void afterStep(Scenario scenario) {
// Runs after each step (useful for debugging)
if (scenario.isFailed()) {
System.out.println("Step failed in: " + scenario.getName());
}
}
@AfterAll
public static void afterAll() {
// Runs once after all scenarios
System.out.println("Test suite execution completed");
DatabaseUtils.cleanupTestDatabase();
}
}
TypeScript Hooks Implementation
// features/support/hooks.ts
import { Before, After, BeforeAll, AfterAll, Status } from '@cucumber/cucumber';
import { chromium, Browser, Page } from '@playwright/test';
let browser: Browser;
let page: Page;
BeforeAll(async function() {
console.log('Initializing test suite');
// Global setup
});
Before(async function(scenario) {
console.log(`Starting: ${scenario.pickle.name}`);
browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
page = await context.newPage();
this.page = page;
});
Before({ tags: '@api' }, async function() {
// Setup for API tests
this.apiClient = createApiClient();
});
After(async function(scenario) {
if (scenario.result?.status === Status.FAILED) {
const screenshot = await page.screenshot();
this.attach(screenshot, 'image/png');
}
await browser.close();
});
AfterAll(async function() {
console.log('Test suite completed');
// Global cleanup
});
Advanced Reporting Strategies
Comprehensive reporting provides insights into test execution and results.
Cucumber Report Configuration
<!-- Maven pom.xml -->
<plugin>
<groupId>net.masterthought</groupId>
<artifactId>maven-cucumber-reporting</artifactId>
<version>5.7.5</version>
<executions>
<execution>
<id>execution</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<projectName>E-Commerce Test Suite</projectName>
<outputDirectory>${project.build.directory}/cucumber-reports</outputDirectory>
<inputDirectory>${project.build.directory}/cucumber-json</inputDirectory>
<jsonFiles>
<param>**/*.json</param>
</jsonFiles>
<buildNumber>42</buildNumber>
<checkBuildResult>true</checkBuildResult>
</configuration>
</execution>
</executions>
</plugin>
Test Runner Configuration
// src/test/java/runners/TestRunner.java
package runners;
import org.junit.runner.RunWith;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue = {"steps", "hooks"},
tags = "@smoke or @regression",
plugin = {
"pretty",
"html:target/cucumber-reports/cucumber.html",
"json:target/cucumber-reports/cucumber.json",
"junit:target/cucumber-reports/cucumber.xml",
"com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:"
},
monochrome = true,
dryRun = false
)
public class TestRunner {
// Test runner class
}
Parallel Execution Configuration
<!-- Maven Surefire for parallel execution -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<parallel>methods</parallel>
<threadCount>4</threadCount>
<perCoreThreadCount>true</perCoreThreadCount>
<includes>
<include>**/TestRunner*.java</include>
</includes>
</configuration>
</plugin>
Report Types Comparison
Report Type | Format | Use Case | Features |
---|---|---|---|
HTML Report | Browser-viewable | Human review | Interactive, screenshots |
JSON Report | Machine-readable | CI/CD integration | Parseable, detailed |
JUnit XML | XML | Jenkins/CI tools | Standard format |
Extent Report | Rich HTML | Stakeholder demos | Charts, filters, tags |
Allure Report | Interactive HTML | Detailed analysis | History, trends, categories |
Practical Use Case: E-Commerce Checkout Flow
# features/checkout/complete_purchase.feature
@checkout @critical
Feature: Complete Purchase Flow
As a customer
I want to complete my purchase
So that I can receive my ordered products
Background:
Given the following products exist:
| Product ID | Name | Price | Stock |
| PROD001 | Laptop Pro | 999.99 | 10 |
| PROD002 | Wireless Mouse | 29.99 | 50 |
And I am logged in as "premium_customer@example.com"
@smoke @checkout-success
Scenario: Successful checkout with credit card
Given I have the following items in my cart:
| Product ID | Quantity |
| PROD001 | 1 |
| PROD002 | 2 |
When I proceed to checkout
And I select shipping address:
| Street | 123 Main St |
| City | San Francisco |
| State | CA |
| Zip | 94105 |
| Country | USA |
And I select "Standard Shipping" delivery option
And I pay with credit card:
| Card Number | 4532-1234-5678-9010 |
| Expiry | 12/25 |
| CVV | 123 |
| Name | John Doe |
Then the order should be confirmed
And I should receive order confirmation email
And the order total should be "$1,059.97"
And the inventory should be updated:
| Product ID | New Stock |
| PROD001 | 9 |
| PROD002 | 48 |
@checkout-validation
Scenario Outline: Payment validation
Given I have "PROD001" in my cart
When I proceed to checkout
And I enter payment details:
| Card Number | <card_number> |
| Expiry | <expiry> |
| CVV | <cvv> |
Then I should see "<error_message>"
Examples:
| card_number | expiry | cvv | error_message |
| 1234-5678-9012-3456 | 12/25 | 123 | Invalid card number |
| 4532-1234-5678-9010 | 12/20 | 123 | Card expired |
| 4532-1234-5678-9010 | 12/25 | 12 | Invalid CVV |
Best Practices for Cucumber BDD
1. Writing Effective Scenarios
# Good: Declarative, business-focused
Scenario: Customer views order history
Given I am a logged-in customer
When I navigate to order history
Then I should see my past orders
# Bad: Imperative, implementation-focused
Scenario: Customer views order history
Given I click on the profile icon
When I click on "Order History" link
And I wait for 2 seconds
Then I should see a table with class "order-table"
2. Reusable Step Definitions
// Good: Flexible, reusable steps
@Given("I am logged in as {string}")
public void loginAs(String userType) {
User user = UserFactory.get(userType);
authService.login(user);
}
// Bad: Hardcoded, specific steps
@Given("I am logged in as admin")
public void loginAsAdmin() {
authService.login("admin@example.com", "admin123");
}
3. Tag Organization Strategy
# Effective tag hierarchy
@smoke @critical @authentication
Scenario: Login with valid credentials
@regression @payment @integration
Scenario: Process payment with credit card
@api @integration @orders
Scenario: Create order via API
Conclusion
Cucumber BDD automation transforms software testing from a purely technical activity into a collaborative process that aligns development with business objectives. By using Gherkin’s natural language syntax, teams can create living documentation that serves as both specification and automated tests.
Key advantages of Cucumber BDD:
- Collaboration: Bridges communication gap between technical and non-technical team members
- Living Documentation: Feature files serve as up-to-date system documentation
- Reusability: Step definitions can be reused across multiple scenarios
- Maintainability: Business-readable scenarios are easier to maintain than code-heavy tests
- Traceability: Direct mapping between requirements and test scenarios
By following best practices for feature file organization, step definition patterns, effective use of data tables and hooks, and comprehensive reporting, teams can build robust BDD automation frameworks that deliver value throughout the software development lifecycle.
Whether you’re testing web applications, mobile apps, or APIs, Cucumber provides the foundation for behavior-driven testing that keeps your team aligned and your software quality high.