Frameworks & Patterns
Cucumber BDD Guide

Cucumber BDD Guide: Behavior-Driven Development for Testing

Parul Dhingra - Senior Quality Analyst
Parul Dhingra13+ Years ExperienceHire Me

Senior Quality Analyst

Updated: 1/23/2026

Behavior-Driven Development (BDD) bridges the gap between business stakeholders and technical teams by using a shared language to describe software behavior. Cucumber implements BDD by letting you write tests in plain English (or other languages) that anyone can understand, then automating those tests with code.

This guide covers Cucumber from basic concepts to advanced patterns, with examples in Java, JavaScript, and Python.

What is BDD and Cucumber?

Behavior-Driven Development (BDD) is a development approach that:

  • Focuses on business value and user behavior
  • Uses a shared language between stakeholders and developers
  • Creates executable specifications that serve as documentation
  • Encourages collaboration before implementation

Cucumber is a tool that:

  • Parses Gherkin feature files (plain text specifications)
  • Executes code (step definitions) linked to each step
  • Reports pass/fail status in business terms
  • Supports multiple programming languages

BDD Workflow

┌─────────────────────────────────────────────────────────────┐
│  1. Discovery: Discuss behavior with stakeholders           │
│     "What should happen when a user tries to login?"       │
└──────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  2. Formulation: Write scenarios in Gherkin                 │
│     Given a registered user                                │
│     When they enter valid credentials                       │
│     Then they should see the dashboard                      │
└──────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  3. Automation: Implement step definitions                  │
│     @When("they enter valid credentials")                  │
│     public void enterCredentials() { ... }                 │
└──────────────────────────────────────────────────────────────┘

Gherkin Syntax

Gherkin is the language for writing Cucumber specifications.

Basic Structure

# features/login.feature
 
Feature: User Login
  As a registered user
  I want to log into my account
  So that I can access my personalized dashboard
 
  Background:
    Given the login page is displayed
 
  Scenario: Successful login with valid credentials
    Given a user "john@example.com" with password "secret123"
    When the user enters their credentials
    And clicks the login button
    Then the user should be redirected to the dashboard
    And a welcome message should be displayed
 
  Scenario: Failed login with invalid password
    Given a user "john@example.com" with password "wrongpassword"
    When the user enters their credentials
    And clicks the login button
    Then an error message "Invalid credentials" should be displayed
    And the user should remain on the login page

Keywords

KeywordPurpose
FeatureDescribes the feature being tested
ScenarioA single test case with steps
GivenPreconditions, setup
WhenAction being tested
ThenExpected outcome
And/ButAdditional steps (same type as previous)
BackgroundSteps run before each scenario
Scenario OutlineTemplate for parameterized tests
ExamplesData for scenario outlines

Gherkin keywords have translations for many languages. You can write scenarios in French, Spanish, Japanese, and dozens of other languages.

Setting Up Cucumber

Java Setup (Maven)

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>7.15.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>7.15.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>1.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

JavaScript Setup (npm)

npm install --save-dev @cucumber/cucumber
// cucumber.js
module.exports = {
  default: {
    require: ['features/step_definitions/**/*.js'],
    format: ['progress', 'html:reports/cucumber-report.html'],
  },
};

Python Setup

pip install behave

Project Structure

project/
├── src/main/java/           # Application code
├── src/test/
│   ├── java/
│   │   └── com/example/
│   │       └── steps/
│   │           └── LoginSteps.java
│   └── resources/
│       └── features/
│           └── login.feature
└── pom.xml

Step Definitions

Step definitions link Gherkin steps to code.

Java Step Definitions

// src/test/java/com/example/steps/LoginSteps.java
package com.example.steps;
 
import io.cucumber.java.en.*;
import static org.junit.jupiter.api.Assertions.*;
 
public class LoginSteps {
 
    private String currentUser;
    private String currentPassword;
    private String loginResult;
 
    @Given("the login page is displayed")
    public void loginPageIsDisplayed() {
        // Navigate to login page
        System.out.println("Navigating to login page");
    }
 
    @Given("a user {string} with password {string}")
    public void aUserWithPassword(String email, String password) {
        this.currentUser = email;
        this.currentPassword = password;
    }
 
    @When("the user enters their credentials")
    public void userEntersCredentials() {
        // Fill in form fields
        System.out.println("Entering: " + currentUser);
    }
 
    @When("clicks the login button")
    public void clicksLoginButton() {
        // Click submit
        loginResult = authenticate(currentUser, currentPassword);
    }
 
    @Then("the user should be redirected to the dashboard")
    public void shouldRedirectToDashboard() {
        assertEquals("success", loginResult);
    }
 
    @Then("an error message {string} should be displayed")
    public void errorMessageDisplayed(String expectedMessage) {
        assertEquals(expectedMessage, loginResult);
    }
 
    private String authenticate(String user, String pass) {
        // Simulate authentication
        return pass.equals("secret123") ? "success" : "Invalid credentials";
    }
}

JavaScript Step Definitions

// features/step_definitions/loginSteps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
 
let currentUser, currentPassword, loginResult;
 
Given('the login page is displayed', function () {
  // Navigate to login page
});
 
Given('a user {string} with password {string}', function (email, password) {
  currentUser = email;
  currentPassword = password;
});
 
When('the user enters their credentials', function () {
  // Fill form
});
 
When('clicks the login button', function () {
  loginResult = authenticate(currentUser, currentPassword);
});
 
Then('the user should be redirected to the dashboard', function () {
  expect(loginResult).to.equal('success');
});
 
Then('an error message {string} should be displayed', function (message) {
  expect(loginResult).to.equal(message);
});
 
function authenticate(user, pass) {
  return pass === 'secret123' ? 'success' : 'Invalid credentials';
}

Parameter Types

Given a user "john@example.com" with password "secret"
Given the product costs $29.99
Given the quantity is 5
Given the feature is enabled
// String parameters
@Given("a user {string} with password {string}")
public void userWithPassword(String email, String password) { }
 
// Numeric parameters
@Given("the product costs ${double}")
public void productCosts(Double price) { }
 
@Given("the quantity is {int}")
public void quantity(Integer qty) { }
 
// Boolean (custom)
@Given("the feature is enabled")
public void featureEnabled() { }

Scenario Outlines

Scenario Outlines run the same scenario with different data.

Feature: Login Validation
 
  Scenario Outline: Login with various credentials
    Given a user with email "<email>" and password "<password>"
    When the user attempts to login
    Then the result should be "<result>"
 
    Examples:
      | email              | password    | result              |
      | valid@example.com  | correct123  | success             |
      | valid@example.com  | wrong       | Invalid credentials |
      | invalid            | any         | Invalid email       |
      |                    | password    | Email required      |
 
  Scenario Outline: Password strength validation
    Given a user enters password "<password>"
    Then password strength should be "<strength>"
 
    Examples: Weak passwords
      | password | strength |
      | 123456   | weak     |
      | password | weak     |
 
    Examples: Strong passwords
      | password      | strength |
      | Str0ng!Pass   | strong   |
      | C0mplex#2024  | strong   |
@Given("a user with email {string} and password {string}")
public void userWithCredentials(String email, String password) {
    this.email = email;
    this.password = password;
}
 
@When("the user attempts to login")
public void attemptLogin() {
    result = loginService.authenticate(email, password);
}
 
@Then("the result should be {string}")
public void verifyResult(String expectedResult) {
    assertEquals(expectedResult, result);
}

Hooks and Tags

Hooks

Hooks run before/after scenarios or steps:

import io.cucumber.java.*;
 
public class Hooks {
 
    @Before
    public void beforeScenario(Scenario scenario) {
        System.out.println("Starting: " + scenario.getName());
    }
 
    @After
    public void afterScenario(Scenario scenario) {
        if (scenario.isFailed()) {
            // Take screenshot, log details
        }
        System.out.println("Finished: " + scenario.getName());
    }
 
    @BeforeStep
    public void beforeStep() {
        // Runs before each step
    }
 
    @AfterStep
    public void afterStep() {
        // Runs after each step
    }
}

Tags

Tags organize and filter scenarios:

@smoke @login
Feature: User Login
 
  @happy-path
  Scenario: Successful login
    Given valid credentials
    When user logs in
    Then dashboard is displayed
 
  @negative @security
  Scenario: Login with SQL injection attempt
    Given malicious input
    When user attempts login
    Then request should be rejected

Running tagged scenarios:

# Run only smoke tests
cucumber --tags "@smoke"
 
# Run smoke OR login
cucumber --tags "@smoke or @login"
 
# Run smoke AND login
cucumber --tags "@smoke and @login"
 
# Exclude slow tests
cucumber --tags "not @slow"

Tagged hooks:

@Before("@database")
public void setupDatabase() {
    // Only runs for scenarios tagged @database
}
 
@After("@browser")
public void closeBrowser() {
    // Only runs for scenarios tagged @browser
}

Data Tables

Data tables pass structured data to steps:

Scenario: Create multiple users
  Given the following users exist:
    | email              | name    | role   |
    | admin@example.com  | Admin   | admin  |
    | user@example.com   | User    | member |
    | guest@example.com  | Guest   | guest  |
  When I view the user list
  Then I should see 3 users
import io.cucumber.datatable.DataTable;
import java.util.List;
import java.util.Map;
 
@Given("the following users exist:")
public void usersExist(DataTable dataTable) {
    // As list of maps
    List<Map<String, String>> users = dataTable.asMaps();
    for (Map<String, String> user : users) {
        String email = user.get("email");
        String name = user.get("name");
        String role = user.get("role");
        userService.create(email, name, role);
    }
}
 
// Alternative: Use a custom type
@Given("the following users exist:")
public void usersExist(List<User> users) {
    users.forEach(userService::create);
}

Vertical Tables

Scenario: User profile
  Given a user with details:
    | name  | John Doe          |
    | email | john@example.com  |
    | phone | 555-1234          |
@Given("a user with details:")
public void userWithDetails(Map<String, String> details) {
    String name = details.get("name");
    String email = details.get("email");
    String phone = details.get("phone");
}

Sharing State

Dependency Injection (Java)

// Use PicoContainer (built-in) or other DI frameworks
 
public class ScenarioContext {
    private String currentUser;
    private WebDriver driver;
 
    // Getters and setters
}
 
public class LoginSteps {
    private final ScenarioContext context;
 
    public LoginSteps(ScenarioContext context) {
        this.context = context;
    }
 
    @When("the user logs in as {string}")
    public void loginAs(String user) {
        context.setCurrentUser(user);
    }
}
 
public class DashboardSteps {
    private final ScenarioContext context;
 
    public DashboardSteps(ScenarioContext context) {
        this.context = context;
    }
 
    @Then("the dashboard shows user {string}")
    public void dashboardShowsUser(String expectedUser) {
        assertEquals(expectedUser, context.getCurrentUser());
    }
}

World Object (JavaScript)

// features/support/world.js
const { setWorldConstructor } = require('@cucumber/cucumber');
 
class CustomWorld {
  constructor() {
    this.currentUser = null;
    this.response = null;
  }
 
  async login(user, password) {
    this.response = await api.login(user, password);
    this.currentUser = user;
  }
}
 
setWorldConstructor(CustomWorld);
 
// In steps
Given('user {string} is logged in', async function (user) {
  await this.login(user, 'password');
});

Best Practices

Write Scenarios from User Perspective

# Good - user-focused
Scenario: User purchases a product
  Given I have added a laptop to my cart
  When I complete the checkout process
  Then I should receive an order confirmation email
 
# Avoid - implementation-focused
Scenario: Test checkout API
  Given POST /cart with productId=123
  When POST /checkout with paymentMethod=credit
  Then response status is 200

Keep Steps Declarative

# Good - declarative
Given a logged-in user with items in cart
 
# Avoid - imperative
Given I navigate to login page
And I enter "user@test.com" in email field
And I enter "password" in password field
And I click login button
And I wait for dashboard
And I click cart icon
And I add first item

Use Background for Common Setup

Feature: Shopping Cart
 
  Background:
    Given a logged-in customer
    And the product catalog is loaded
 
  Scenario: Add item to cart
    When I add "Laptop" to my cart
    Then my cart should contain 1 item
 
  Scenario: Remove item from cart
    Given my cart contains a "Laptop"
    When I remove "Laptop" from my cart
    Then my cart should be empty

Reuse Step Definitions

// Create reusable, parameterized steps
@Given("I am logged in as {string}")
@Given("{string} is logged in")
@Given("a logged-in {string}")
public void loggedInAs(String userType) {
    User user = testUsers.get(userType);
    loginPage.login(user.email, user.password);
}

Organize Features by Capability

features/
├── authentication/
│   ├── login.feature
│   ├── logout.feature
│   └── password_reset.feature
├── shopping/
│   ├── cart.feature
│   ├── checkout.feature
│   └── wishlist.feature
└── account/
    ├── profile.feature
    └── preferences.feature

Cucumber and BDD transform testing from a technical activity into a collaborative process that ensures software meets business needs. By writing tests in plain language that everyone can understand, you create living documentation that stays in sync with your application's actual behavior.

Quiz on Cucumber BDD

Your Score: 0/10

Question: What is the primary purpose of BDD (Behavior-Driven Development)?

Continue Reading

Frequently Asked Questions (FAQs) / People Also Ask (PAA)

Is Cucumber only for acceptance testing?

What's the difference between Cucumber and Behave?

Should step definitions contain assertions?

How do I share state between steps?

Can I use Cucumber with Selenium?

How granular should scenarios be?

What's the difference between Examples and Data Tables?

How do I generate reports from Cucumber runs?