What Is BDD?
Behavior-Driven Development (BDD) is a collaboration practice that bridges the gap between business stakeholders, developers, and testers. It uses a structured natural language called Gherkin to describe expected system behavior.
The core idea: define what the system should do (behavior) before implementing how it does it (code).
Gherkin Syntax
Gherkin uses three primary keywords to structure scenarios:
- Given — the precondition (starting state)
- When — the action (what the user does)
- Then — the expected outcome (what should happen)
Feature File Example
# features/login.feature
Feature: User Login
As a registered user
I want to login to my account
So that I can access my dashboard
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter "admin@test.com" as email
And I enter "secret123" as password
And I click the login button
Then I should be redirected to the dashboard
And I should see "Welcome, Admin" as the greeting
Scenario: Failed login with wrong password
Given I am on the login page
When I enter "admin@test.com" as email
And I enter "wrongpassword" as password
And I click the login button
Then I should see an error message "Invalid credentials"
And I should remain on the login page
Scenario Outline (Data-Driven BDD)
Scenario Outline: Login with various credentials
Given I am on the login page
When I enter "<email>" as email
And I enter "<password>" as password
And I click the login button
Then I should see "<result>"
Examples:
| email | password | result |
| admin@test.com | secret123 | Welcome, Admin |
| editor@test.com | pass456 | Welcome, Editor |
| wrong@test.com | wrong | Invalid credentials |
| | secret123 | Email is required |
Step Definitions
Step definitions connect Gherkin steps to automation code:
// steps/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('@playwright/test');
let page;
Given('I am on the login page', async function () {
page = this.page;
await page.goto('/login');
});
When('I enter {string} as email', async function (email) {
await page.fill('[data-testid="email"]', email);
});
When('I enter {string} as password', async function (password) {
await page.fill('[data-testid="password"]', password);
});
When('I click the login button', async function () {
await page.click('[data-testid="login-submit"]');
});
Then('I should be redirected to the dashboard', async function () {
await expect(page).toHaveURL('/dashboard');
});
Then('I should see {string} as the greeting', async function (greeting) {
await expect(page.locator('.welcome-msg')).toHaveText(greeting);
});
Then('I should see an error message {string}', async function (message) {
await expect(page.locator('.error-message')).toHaveText(message);
});
Then('I should remain on the login page', async function () {
await expect(page).toHaveURL('/login');
});
Writing Good Gherkin
Business Language, Not Technical Language
# Bad — too technical
Scenario: Login test
Given I navigate to "https://app.example.com/login"
When I fill "#email" with "admin@test.com"
And I fill "#password" with "secret123"
And I click "#login-btn"
Then the URL should be "/dashboard"
# Good — business behavior
Scenario: Successful login
Given I am a registered admin user
When I login with valid credentials
Then I should see my admin dashboard
Declarative Over Imperative
# Imperative (too many steps, hard to read)
Scenario: Purchase a product
Given I open the browser
And I navigate to the homepage
And I click the "Products" link
And I search for "Wireless Mouse"
And I click the first result
And I click "Add to Cart"
And I click the cart icon
And I click "Checkout"
And I enter "4242424242424242" as card number
And I click "Pay"
# Declarative (clear behavior)
Scenario: Purchase a product
Given I am logged in as a customer
When I add "Wireless Mouse" to my cart
And I complete checkout with my saved payment method
Then I should receive an order confirmation
Project Structure
project/
├── features/
│ ├── login.feature
│ ├── checkout.feature
│ ├── search.feature
│ └── support/
│ └── world.js
├── steps/
│ ├── login.steps.js
│ ├── checkout.steps.js
│ ├── search.steps.js
│ └── common.steps.js
├── pages/
│ ├── LoginPage.js
│ ├── CheckoutPage.js
│ └── SearchPage.js
└── cucumber.js (configuration)
Tags for Organization
@smoke @login
Feature: User Login
@positive @critical
Scenario: Successful login
Given I am on the login page
When I login with valid credentials
Then I should see my dashboard
@negative
Scenario: Login with locked account
Given I have a locked account
When I attempt to login
Then I should see an account locked message
Running with tags:
npx cucumber-js --tags "@smoke"
npx cucumber-js --tags "@smoke and not @negative"
npx cucumber-js --tags "@login and @critical"
BDD with Playwright-BDD
Playwright-BDD integrates Cucumber’s Gherkin syntax directly with Playwright:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { defineBddConfig } from 'playwright-bdd';
const testDir = defineBddConfig({
features: 'features/*.feature',
steps: 'steps/*.ts',
});
export default defineConfig({
testDir,
});
// steps/login.ts
import { createBdd } from 'playwright-bdd';
const { Given, When, Then } = createBdd();
Given('I am on the login page', async ({ page }) => {
await page.goto('/login');
});
When('I login with {string} and {string}', async ({ page }, email, password) => {
await page.fill('#email', email);
await page.fill('#password', password);
await page.click('#submit');
});
Then('I should see the dashboard', async ({ page }) => {
await page.waitForURL('/dashboard');
});
Hooks for Setup and Teardown
const { Before, After, BeforeAll, AfterAll } = require('@cucumber/cucumber');
BeforeAll(async function () {
// Launch browser once for all tests
});
Before(async function (scenario) {
// Run before each scenario
console.log(`Starting: ${scenario.pickle.name}`);
this.page = await this.browser.newPage();
});
After(async function (scenario) {
// Run after each scenario
if (scenario.result.status === 'FAILED') {
await this.page.screenshot({
path: `screenshots/${scenario.pickle.name}.png`
});
}
await this.page.close();
});
AfterAll(async function () {
// Close browser after all tests
});
BDD Anti-Patterns
1. Writing Features After Code
BDD is a collaboration tool. Writing features after implementation defeats the purpose. Features should be written before development as specifications.
2. Too Many Scenarios
A feature with 50 scenarios is a sign that scenarios are too granular or the feature is too broad. Aim for 5-15 scenarios per feature.
3. Cucumber as Just a Test Tool
BDD is about collaboration, not just test automation. If only QA writes Gherkin files, you are missing the biggest benefit.
4. Brittle Step Definitions
Step definitions coupled to specific UI elements break when the UI changes. Use page objects inside step definitions.
The Three Amigos Meeting
BDD’s greatest value comes from the “Three Amigos” meeting where business, development, and QA discuss features together:
- Business explains the desired behavior
- Development asks about edge cases and technical constraints
- QA identifies scenarios that might be missed
The output: a set of Gherkin scenarios that everyone agrees on before a single line of code is written.
Exercise: Write a BDD Feature
Create a complete BDD feature file for an e-commerce shopping cart:
- Write a feature description (As a… I want… So that…)
- Write 5 scenarios: add item, remove item, update quantity, apply discount code, proceed to checkout
- Use a Scenario Outline with Examples for testing different discount codes
- Add tags for smoke, regression, and positive/negative categorization
- Write step definitions for at least 3 scenarios using page objects
Key Takeaways
- BDD uses Gherkin (Given/When/Then) to describe behavior in business language
- Feature files serve as living documentation that everyone can understand
- Step definitions connect Gherkin to automation code
- Write declarative scenarios focused on behavior, not imperative steps
- The Three Amigos meeting is where BDD’s collaboration value is realized
- Use tags to organize and selectively run scenarios