What Is BDD
BDD bridges the gap between business stakeholders and developers by expressing requirements as executable specifications. Instead of writing tests after the fact, you describe the expected behavior of the system in plain language, then automate those descriptions.
The core loop: Discovery (what should the system do?) → Formulation (write it in Gherkin) → Automation (implement step definitions).
BDD vs TDD
TDD focuses on unit-level correctness (“does this function return the right value?”). BDD focuses on system-level behavior (“does this feature work from the user’s perspective?”). In practice they complement each other — use TDD for internal logic, BDD for acceptance and integration tests.
Cucumber
Cucumber is the most widely adopted BDD framework. It reads .feature files written in Gherkin and executes matching step definitions.
Supported languages: Java, JavaScript/TypeScript (cucumber-js), Ruby, Python (behave), Kotlin, .NET (SpecFlow), Go (godog).
Installing cucumber-js
npm install --save-dev @cucumber/cucumber
For TypeScript, add tsx for just-in-time transpilation:
npm install --save-dev @cucumber/cucumber tsx
In cucumber.js config:
module.exports = {
default: {
requireModule: ["tsx"],
require: ["features/step_definitions/**/*.ts"],
format: ["progress", "html:reports/cucumber.html"],
},
};
Gherkin Syntax Reference
Gherkin uses keywords to structure specifications. Each .feature file describes one feature with one or more scenarios.
Keywords
| Keyword | Purpose |
|---|---|
Feature |
Top-level description of a feature |
Rule |
Groups scenarios under a business rule (Gherkin 6+) |
Scenario |
A single concrete example of behavior |
Given |
Precondition — set the context |
When |
Action — the event being tested |
Then |
Outcome — the expected result |
And / But |
Continue the previous step type |
Background |
Steps shared by all scenarios in a feature |
Scenario Outline |
Template with variable examples |
Examples |
Data table for Scenario Outline |
Feature File Example
Feature: User authentication
Background:
Given the user is on the login page
Scenario: Successful login
When the user enters valid credentials
Then the user is redirected to the dashboard
And a welcome message is displayed
Scenario: Invalid password
When the user enters an invalid password
Then an error message "Invalid credentials" is shown
And the user remains on the login page
Scenario Outline with Examples
Feature: Shopping cart pricing
Scenario Outline: Discount tiers
Given a cart with <quantity> items at $<price> each
When the checkout total is calculated
Then the discount should be <discount>%
And the total should be $<total>
Examples:
| quantity | price | discount | total |
| 1 | 10.00 | 0 | 10.00 |
| 5 | 10.00 | 10 | 45.00 |
| 10 | 10.00 | 20 | 80.00 |
Data Tables and Doc Strings
Scenario: Register multiple users
Given the following users exist:
| name | email | role |
| Alice | alice@example.com | admin |
| Bob | bob@example.com | viewer |
Scenario: Submit a JSON payload
When the client sends a POST to "/api/data" with body:
"""json
{
"name": "test",
"value": 42
}
"""
Then the response status is 201
Tags
Tags filter which scenarios run. Prefix with @:
@smoke @auth
Feature: Authentication
@automated
Scenario: Login with SSO
...
@manual @wip
Scenario: Login with biometrics
...
Run only automated smoke tests:
npx cucumber-js --tags "@smoke and @automated"
Step Definitions (TypeScript)
import { Given, When, Then } from "@cucumber/cucumber";
import { expect } from "chai";
Given("the user is on the login page", async function () {
await this.page.goto("/login");
});
When("the user enters valid credentials", async function () {
await this.page.fill("#email", "user@test.com");
await this.page.fill("#password", "secret");
await this.page.click('button[type="submit"]');
});
Then("the user is redirected to the dashboard", async function () {
expect(this.page.url()).to.include("/dashboard");
});
Then("an error message {string} is shown", async function (message: string) {
const alert = await this.page.textContent(".alert-error");
expect(alert).to.equal(message);
});
Best Practices
Write declarative, not imperative. Describe what the user does, not how they interact with the UI. Bad: “When the user clicks the #login-btn element.” Good: “When the user logs in.”
One When-Then pair per scenario. Multiple When-Then blocks signal that the scenario covers too many behaviors. Split it.
Keep steps reusable. Avoid conjunctive steps (“Given the user is logged in and has items in cart”). Break into two steps — each one becomes reusable across scenarios.
Use Background sparingly. Only for Given steps shared by every scenario in the feature. If only some scenarios need it, duplicate the Given or split into separate features.
Verify observable outcomes. Then steps should check what the user (or an external system) can see — not internal state like database rows.
Tag strategy. Use @automated, @manual, @wip to manage test execution. In CI, run --tags "not @manual and not @wip".
Three Amigos. Before writing Gherkin, hold a brief session with a developer, tester, and product person to agree on scenarios. This is where BDD delivers the most value — shared understanding before code.
When to Use BDD (and When Not To)
Use BDD for: API acceptance tests, UI integration tests, cross-team specifications, user-facing features, regression suites.
Skip BDD for: unit tests (use TDD directly), performance benchmarks, one-off scripts, purely technical refactors.