Skip to content
← All insights
Behaviour-driven development15 min read

Behaviour-driven development: use examples to make software decisions testable

How behaviour-driven development uses concrete examples to create shared understanding, guide implementation and provide useful automated evidence—without turning every acceptance criterion into a brittle browser test.

BDD is a conversation before it is a test framework

Behaviour-driven development starts from a shared question: what should this capability do for a user or another system, and how will we recognise that it works? Product, delivery and engineering perspectives are all needed because each sees a different failure mode.

A Cucumber feature file is useful only when it records the outcome of that conversation. Given, When and Then are a compact language for context, action and observable result; they are not a requirement to write prose around every line of code.

The purpose is to reduce expensive ambiguity before implementation begins. If a team cannot agree on examples of valid, invalid and boundary behaviour, a passing automated test will not resolve the underlying decision.

Start with examples of behaviour, not implementation steps

A useful scenario describes an outcome in the language of the domain. It does not tell the user interface where to click, which database row to insert or which private method must be called. Those are implementation details that make a specification fragile.

Use a specific example to expose a rule. “A customer with an overdue invoice cannot place a new order” is clearer than “the system validates customer status”. The examples can then reveal questions about dates, partial payment, permissions and what the user should see.

Keep a scenario small enough to explain one rule. A long script that creates an account, changes a preference, buys a product, waits for an email and checks a report is usually several behaviours hidden behind one slow failure.

Describe a business rule in observable termsgherkin
Feature: Order eligibility

  Scenario: An account with overdue debt cannot place an order
    Given an account with an overdue invoice
    When the account holder submits a new order
    Then the order is not accepted
    And the account holder is told that payment is required

Use Given, When and Then precisely

Given establishes relevant context. It should state the meaningful state of the world, not the click sequence used to create it. Test setup through an API, fixture or direct database helper can be acceptable when it keeps the scenario focused on the rule being proved.

When performs one material action. Several When steps often mean the scenario is testing a workflow rather than a decision; that can be valid, but it should be deliberate and short.

Then checks the externally observable result: a response, status, event, document or screen state. Avoid asserting internal objects or mocks at this level. If the only proof is that a private method was called, the scenario is coupled to the implementation rather than the behaviour.

Examples expose edge cases better than abstract rules

Rules written as broad statements conceal boundaries. A concrete example forces a team to decide what happens when an amount is exactly zero, an entitlement expires today, an event arrives twice or a customer has two roles.

Use a scenario outline when the same rule genuinely applies to several values. The table should make the domain distinction visible, not become a disguised data dump. A handful of representative examples is often clearer than every possible combination.

When an example raises a policy question, record the answer where the team can find it. The scenario should be the durable evidence of the agreed behaviour, not the only record of why that decision was made.

Show the boundary through examplesgherkin
Scenario Outline: A payment must be positive
  When a payment of <amount> is submitted
  Then the payment is <outcome>

  Examples:
    | amount | outcome  |
    | 10.00  | accepted |
    | 0.00   | rejected |
    | -1.00  | rejected |

Put automated scenarios at the right boundary

A BDD scenario can drive a domain service, an HTTP API or a browser. Choose the narrowest boundary that proves the behaviour users care about. A domain rule usually does not need a browser; a journey involving authentication and client-side validation may need one or two browser examples.

Browser-driven scenarios are valuable but expensive. They require stable test data, controlled authentication, reliable waiting and an environment close enough to production to reveal integration failures. Use them for a few representative journeys rather than every rule.

The BDD language can remain the shared specification even when the implementation runs below the UI. A scenario that drives an API can still be read by product and delivery colleagues, and it will usually run faster than its browser equivalent.

Keep step definitions thin and intention-revealing

Step definitions translate the shared language into test setup and calls to the application. They should delegate to helpers, fixtures and application-facing APIs rather than holding business logic themselves. If the rule lives in a step definition, unit and integration tests may not protect it elsewhere.

Avoid generic steps such as “Given I do something” that can mean anything in different features. Reusable vocabulary is useful, but excessive reuse creates a hidden programming language that readers must decode before they can understand a scenario.

Treat a feature file and its step definitions as production code. Name concepts consistently, remove dead steps and review changes with the same care as an application change.

Use BDD alongside, not instead of, other tests

BDD scenarios give confidence that an important example works from the chosen boundary. They do not replace focused unit tests for many permutations, integration tests for persistence behaviour or contract tests for service interactions.

A good test portfolio keeps feedback fast. A small number of scenarios can prove the important business journeys; lower-level tests can explore calculations, validation combinations and failure handling much more quickly.

When a scenario fails, ask what decision the failure invalidates. A failing test is useful when it tells the team whether the rule, implementation, data or environment needs attention.

Make scenarios part of delivery planning

Discuss examples during refinement or discovery, before work enters implementation. The result should be a small set of behaviours that define what “done” means, plus open questions that need a decision before the team can commit.

A scenario is not an estimate and does not remove uncertainty. It helps make uncertainty visible: a dependency may not exist, the policy may be undecided or reliable test data may be unavailable. Those are delivery risks to resolve, not details to hide in a ticket.

Review the scenarios after release when behaviour changes. Outdated specifications are worse than absent ones because they confidently describe a product that no longer exists.

  • Write scenarios after a conversation with the people who own the decision
  • Describe observable outcomes rather than screen clicks or private methods
  • Use examples to make boundary cases explicit
  • Run scenarios at the narrowest boundary that proves the behaviour
  • Keep browser scenarios few, stable and representative
  • Use BDD scenarios with unit, integration and contract tests—not instead of them