Testing

Behavior-Driven Development

A development approach that expresses test cases in plain language using Given-When-Then format, bridging communication between developers and stakeholders.

What Is Behavior-Driven Development?

Behavior-driven development (BDD) is a software development methodology that extends test-driven development by expressing test cases in structured natural language that non-technical stakeholders can read and validate. Instead of writing tests in code that only developers understand, BDD uses a Given-When-Then format that describes the expected behavior of the system in plain English.

BDD was created by Dan North in 2006 as a response to the confusion he observed around TDD. Developers struggled with questions like “Where do I start?” and “What do I test?” BDD answers these questions by shifting the focus from testing implementation to specifying behavior. The word “test” is replaced with “specification” or “scenario,” and the starting point is always a business requirement described in language that everyone on the team can understand.

The central idea behind BDD is that software teams waste time building the wrong thing because requirements are ambiguous and developers interpret them differently than product managers or QA engineers. BDD eliminates this ambiguity by creating a shared specification language — the Given-When-Then format — that serves as both a requirements document and an executable test suite.

How It Works

BDD scenarios are written in a format called Gherkin, which structures behavior descriptions using keywords: Feature, Scenario, Given, When, Then, and And. These files are typically saved with a .feature extension and are backed by step definition code that executes the actual tests.

Here is a Gherkin feature file for a shopping cart:

# cart.feature
Feature: Shopping Cart
  As a customer
  I want to manage items in my shopping cart
  So that I can purchase products I need

  Scenario: Adding an item to an empty cart
    Given the cart is empty
    When I add "Wireless Mouse" priced at $29.99
    Then the cart should contain 1 item
    And the cart total should be $29.99

  Scenario: Applying a discount code
    Given the cart contains "Keyboard" priced at $79.99
    When I apply discount code "SAVE10"
    Then the cart total should be $71.99

  Scenario: Removing an item from the cart
    Given the cart contains "Monitor" priced at $299.99
    And the cart contains "Cable" priced at $9.99
    When I remove "Cable" from the cart
    Then the cart should contain 1 item
    And the cart total should be $299.99

The step definitions that implement these scenarios vary by language. In JavaScript with Cucumber.js:

// cart.steps.js
const { Given, When, Then } = require("@cucumber/cucumber");
const { expect } = require("chai");
const Cart = require("../src/cart");

let cart;

Given("the cart is empty", function () {
  cart = new Cart();
});

Given("the cart contains {string} priced at ${float}", function (name, price) {
  cart = cart || new Cart();
  cart.addItem({ name, price });
});

When("I add {string} priced at ${float}", function (name, price) {
  cart.addItem({ name, price });
});

When("I apply discount code {string}", function (code) {
  cart.applyDiscount(code);
});

When("I remove {string} from the cart", function (name) {
  cart.removeItem(name);
});

Then("the cart should contain {int} item(s)", function (count) {
  expect(cart.itemCount()).to.equal(count);
});

Then("the cart total should be ${float}", function (total) {
  expect(cart.total()).to.be.closeTo(total, 0.01);
});

In Python using behave:

# steps/cart_steps.py
from behave import given, when, then
from cart import Cart

@given('the cart is empty')
def step_cart_is_empty(context):
    context.cart = Cart()

@given('the cart contains "{name}" priced at ${price:f}')
def step_cart_contains_item(context, name, price):
    if not hasattr(context, 'cart'):
        context.cart = Cart()
    context.cart.add_item(name, price)

@when('I add "{name}" priced at ${price:f}')
def step_add_item(context, name, price):
    context.cart.add_item(name, price)

@then('the cart should contain {count:d} item(s)')
def step_cart_item_count(context, count):
    assert context.cart.item_count() == count

@then('the cart total should be ${total:f}')
def step_cart_total(context, total):
    assert abs(context.cart.total() - total) < 0.01

Running npx cucumber-js or behave executes the feature files against the step definitions, producing human-readable output that shows which scenarios passed or failed.

Why It Matters

BDD bridges the communication gap between technical and non-technical team members. Product managers can read the .feature files and verify that the described behavior matches the business requirements — without needing to understand code. QA engineers can write new scenarios in Gherkin without writing step definitions. Developers implement the step definitions and the production code, and everyone is working from the same source of truth.

This shared understanding dramatically reduces the incidence of building the wrong thing. In traditional development, a product manager writes a requirements document, a developer interprets it (often differently than intended), and the mismatch is not discovered until the feature is already built. BDD catches these mismatches during the specification phase, when they are cheap to fix.

BDD scenarios also serve as living documentation. Unlike static requirements documents that become outdated as soon as the code changes, BDD feature files are executed as tests on every build. If a scenario fails, either the code has a bug or the specification needs updating — either way, the documentation stays accurate.

Best Practices

  • Write scenarios from the user’s perspective. Use business language, not technical language. “Given the user is logged in” is better than “Given the session token is set in the cookie header.” The scenario should describe behavior that a product manager would recognize.
  • Keep scenarios declarative. Describe what the system should do, not how it does it. “When I submit the order” is better than “When I click the submit button and wait for the AJAX response.” Implementation details in scenarios make them fragile.
  • Reuse step definitions. The same step (e.g., “Given the user is logged in”) should use the same step definition across all features. This creates a library of reusable building blocks that makes writing new scenarios fast.
  • Collaborate on scenario writing. BDD scenarios should be written collaboratively in “three amigos” sessions involving a developer, a QA engineer, and a product manager. This ensures the scenarios capture the right behavior from all perspectives.
  • Separate specification from automation. The .feature file should be readable by anyone. The step definition code is a separate concern. Non-technical team members should be able to write and review scenarios without understanding the automation layer.

Common Mistakes

  • Using BDD as a testing tool instead of a collaboration tool. If only developers write the scenarios and no one else reads them, BDD adds overhead without delivering its primary benefit: shared understanding. BDD is a communication practice, not just a testing framework.
  • Writing overly detailed scenarios. Scenarios with 15 Given steps and 10 Then steps are hard to read and maintain. Each scenario should test one specific behavior. If it needs extensive setup, the feature may need to be decomposed into smaller, more focused scenarios.
  • Duplicating unit test logic in BDD. BDD scenarios should describe business behavior at a high level. Low-level logic validation (e.g., “the function returns null when the input is empty”) belongs in unit tests. Using BDD for unit-level testing adds ceremony without value.
  • Not maintaining step definitions. As the codebase evolves, step definitions can become stale or broken. Treat step definition code with the same care as production code — refactor it, keep it DRY, and review it during pull requests.

Related Terms

Learn More

Free Newsletter

Stay ahead with AI dev tools

Weekly insights on AI code review, static analysis, and developer productivity. No spam, unsubscribe anytime.

Join developers getting weekly AI tool insights.