Introduction to BDD & Cucumber

What is BDD, setup, first feature file & runner

What is BDD?

Imagine you’re building a login page. The developer thinks “valid credentials” means any non-empty string. The tester thinks it means an email and password from the database. The product owner thinks it means an active user with a verified email. Three people, three different interpretations — and the bug is born before a single line of code is written.

Behavior-Driven Development (BDD) solves this by making everyone — business, development, and QA — describe the expected behavior in a shared, plain-English format before coding starts.

The Three Amigos

BDD works best when three perspectives come together in a conversation called the Three Amigos meeting:

RoleBringsAsks
Product OwnerBusiness rules & acceptance criteria”What should the system do?”
DeveloperTechnical feasibility & edge cases”What could go wrong technically?”
Tester (QA)Scenarios, edge cases & what-ifs”What if the user does X instead?”

The output of this meeting is a set of scenarios written in a structured format that everyone agrees on. These scenarios become both the specification and the test.

TDD vs BDD vs ATDD

You’ll hear these terms used interchangeably, but they’re different:

AspectTDDBDDATDD
Full NameTest-Driven DevelopmentBehavior-Driven DevelopmentAcceptance Test-Driven Development
FocusUnit-level code correctnessSystem behavior from user perspectiveAcceptance criteria for user stories
Written ByDevelopersThree Amigos (PO + Dev + QA)QA + PO
LanguageProgramming language (Java, Python)Plain English (Gherkin)Plain English or structured
Tool ExampleJUnit, TestNGCucumber, SpecFlowFitNesse, Cucumber
CycleRed → Green → RefactorDiscover → Formulate → AutomateDiscuss → Distill → Develop → Demo

Why Cucumber?

Cucumber is the most popular BDD framework. It takes plain-English scenarios written in Gherkin syntax and connects them to automation code (Java, JavaScript, Ruby, etc.) through step definitions.

How Cucumber Works — The Big Picture

Here’s the flow from requirement to automated test:

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Feature File   │────▶│ Step Definitions  │────▶│  Automation Code │
│  (Gherkin)      │     │  (Java Glue)      │     │  (Selenium, API) │
│                 │     │                   │     │                  │
│ Given I login   │     │ @Given("I login") │     │ driver.get(url)  │
│ When I search   │     │ @When("I search") │     │ page.search()    │
│ Then I see...   │     │ @Then("I see...")  │     │ assert(result)   │
└─────────────────┘     └──────────────────┘     └─────────────────┘
        │                                                 │
        │              ┌──────────────────┐              │
        └─────────────▶│   Test Runner    │◀─────────────┘
                       │  (TestNG/JUnit)  │
                       │  Executes all    │
                       └──────────────────┘
  1. Feature File — Plain-English scenarios in Gherkin format (.feature)
  2. Step Definitions — Java methods that map Gherkin steps to code
  3. Automation Code — The actual actions (click buttons, call APIs, assert results)
  4. Test Runner — Ties everything together and executes the tests

Gherkin at a Glance

Gherkin is the language Cucumber uses. It’s designed to be readable by non-technical people while being structured enough for machines to parse.

Your First Feature File

Feature: User Login
  As a registered user
  I want to log in to my account
  So that I can access my personalized dashboard

  Scenario: Successful login with valid credentials
    Given I am on the login page
    When I enter username "john_doe"
    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 "Hello, John!"

Let’s break this down:

KeywordPurposeIn This Example
FeatureNames the feature being testedUser Login
As a / I want / So thatUser story (optional but helpful)Context for why this matters
ScenarioOne specific test caseSuccessful login with valid credentials
GivenPrecondition — the starting stateI am on the login page
WhenAction — what the user doesI enter username, password, click login
ThenOutcome — the expected resultRedirected to dashboard, see welcome message
AndContinuation of the previous keywordAdditional steps within Given/When/Then

Project Setup

Prerequisites

  • Java 11+ installed (java -version to check)
  • Maven installed (mvn -version to check)
  • An IDE — IntelliJ IDEA (recommended) or Eclipse
  • Cucumber plugin for your IDE (for syntax highlighting)

Step 1: Create a Maven Project

mvn archetype:generate \
  -DgroupId=com.gyancode.cucumber \
  -DartifactId=cucumber-bdd-tutorial \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

Step 2: Add Dependencies to pom.xml

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <cucumber.version>7.15.0</cucumber.version>
    <selenium.version>4.16.0</selenium.version>
</properties>

<dependencies>
    <!-- Cucumber Core -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>${cucumber.version}</version>
    </dependency>

    <!-- Cucumber + TestNG Integration -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-testng</artifactId>
        <version>${cucumber.version}</version>
    </dependency>

    <!-- Selenium WebDriver -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>${selenium.version}</version>
    </dependency>

    <!-- TestNG -->
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.3</version>
            <configuration>
                <includes>
                    <include>**/TestRunner.java</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

Step 3: Project Structure

cucumber-bdd-tutorial/
├── pom.xml
└── src/
    └── test/
        ├── java/
        │   └── com/gyancode/cucumber/
        │       ├── runner/
        │       │   └── TestRunner.java
        │       └── stepdefinitions/
        │           └── LoginSteps.java
        └── resources/
            └── features/
                └── login.feature

Your First Cucumber Test

Let’s write a complete working example — a simple calculator test that doesn’t need Selenium, so you can focus purely on how Cucumber works.

Step 1: The Feature File

Create src/test/resources/features/calculator.feature:

Feature: Calculator
  As a user
  I want to perform basic arithmetic
  So that I can get correct results

  Scenario: Add two numbers
    Given I have a calculator
    When I add 5 and 3
    Then the result should be 8

  Scenario: Subtract two numbers
    Given I have a calculator
    When I subtract 3 from 10
    Then the result should be 7

Step 2: The Step Definitions

Create src/test/java/com/gyancode/cucumber/stepdefinitions/CalculatorSteps.java:

package com.gyancode.cucumber.stepdefinitions;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.testng.Assert;

public class CalculatorSteps {

    private int result;

    @Given("I have a calculator")
    public void iHaveACalculator() {
        // Setup — nothing needed for a simple calculator
        System.out.println("Calculator is ready!");
    }

    @When("I add {int} and {int}")
    public void iAddAnd(int a, int b) {
        result = a + b;
    }

    @When("I subtract {int} from {int}")
    public void iSubtractFrom(int a, int b) {
        result = b - a;
    }

    @Then("the result should be {int}")
    public void theResultShouldBe(int expected) {
        Assert.assertEquals(result, expected,
            "Expected " + expected + " but got " + result);
    }
}

Step 3: The Test Runner

Create src/test/java/com/gyancode/cucumber/runner/TestRunner.java:

package com.gyancode.cucumber.runner;

import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;

@CucumberOptions(
    features = "src/test/resources/features",
    glue = "com.gyancode.cucumber.stepdefinitions",
    plugin = {
        "pretty",
        "html:target/cucumber-reports/cucumber.html"
    },
    monochrome = true
)
public class TestRunner extends AbstractTestNGCucumberTests {
}

Step 4: Run It!

# From the project root
mvn test

You should see output like:

Scenario: Add two numbers
  Given I have a calculator      ✓
  When I add 5 and 3             ✓
  Then the result should be 8    ✓

Scenario: Subtract two numbers
  Given I have a calculator      ✓
  When I subtract 3 from 10     ✓
  Then the result should be 7    ✓

2 Scenarios (2 passed)
6 Steps (6 passed)

Understanding Runner Options

The @CucumberOptions annotation configures everything. Here’s what each option does:

OptionWhat It DoesExample
featuresPath to feature files"src/test/resources/features"
gluePackage(s) containing step definitions"com.gyancode.cucumber.stepdefinitions"
pluginReport formats to generate"pretty", "html:target/report.html"
tagsFilter which scenarios to run"@smoke and not @wip"
monochromeClean console output (no ANSI codes)true
dryRunCheck step mappings without running teststrue for validation only

The dryRun Trick

Set dryRun = true when you write new feature files. Cucumber will tell you which steps are missing without running any tests. It even generates step definition snippets you can copy-paste:

You can implement missing steps with the snippets below:

@Given("I am on the registration page")
public void i_am_on_the_registration_page() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

Adding Selenium — A Real Example

Now let’s connect Cucumber with Selenium WebDriver for browser automation:

Feature File: login.feature

Feature: User Login
  As a registered user
  I want to log in to my account
  So that I can access my dashboard

  Scenario: Successful login
    Given I am on the login page
    When I enter username "testuser"
    And I enter password "password123"
    And I click the login button
    Then I should see the dashboard

  Scenario: Failed login with wrong password
    Given I am on the login page
    When I enter username "testuser"
    And I enter password "wrongpass"
    And I click the login button
    Then I should see an error message "Invalid credentials"

Step Definitions: LoginSteps.java

package com.gyancode.cucumber.stepdefinitions;

import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import java.time.Duration;

public class LoginSteps {
    private WebDriver driver;
    private WebDriverWait wait;

    @Before
    public void setUp() {
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Given("I am on the login page")
    public void iAmOnTheLoginPage() {
        driver.get("https://example.com/login");
    }

    @When("I enter username {string}")
    public void iEnterUsername(String username) {
        WebElement field = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.id("username"))
        );
        field.clear();
        field.sendKeys(username);
    }

    @When("I enter password {string}")
    public void iEnterPassword(String password) {
        WebElement field = driver.findElement(By.id("password"));
        field.clear();
        field.sendKeys(password);
    }

    @When("I click the login button")
    public void iClickTheLoginButton() {
        driver.findElement(By.id("login-btn")).click();
    }

    @Then("I should see the dashboard")
    public void iShouldSeeTheDashboard() {
        wait.until(ExpectedConditions.urlContains("/dashboard"));
        Assert.assertTrue(driver.getCurrentUrl().contains("/dashboard"));
    }

    @Then("I should see an error message {string}")
    public void iShouldSeeAnErrorMessage(String expectedMessage) {
        WebElement error = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.css(".error-message"))
        );
        Assert.assertEquals(error.getText(), expectedMessage);
    }
}

Common Beginner Mistakes

MistakeWhat HappensFix
Wrong glue pathCucumber can’t find step definitions — all steps show as “undefined”Use the full package name: "com.gyancode.cucumber.stepdefinitions"
Feature file not in resourcesCucumber can’t find feature filesPlace .feature files under src/test/resources/features/
Step text doesn’t match exactly”Undefined step” error even though the method existsThe annotation string must match the Gherkin step exactly (including spaces and quotes)
Missing Cucumber dependencyImport errors for @Given, @When, @ThenEnsure cucumber-java is in pom.xml
Runner doesn’t extend AbstractTestNGCucumberTestsTests don’t executeThe runner class must extend the correct base class

Summary