Types of Programming

Behaviour-Driven Development

Specifying software behaviour in human-readable scenarios that act as both docs and tests.

Overview

Behaviour-Driven Development extends TDD by expressing tests in domain language that non-technical stakeholders can read. Dan North introduced BDD in 2006 as a response to TDD practitioners asking "what should I test?" BDD scenarios use Given/When/Then structure (Gherkin syntax) to describe system behaviour from the user's perspective. Cucumber, RSpec, and Jasmine are primary tools.

Origin

Dan North published "Introducing BDD" in Better Software Magazine (2006) after observing that TDD's vocabulary ("test", "assert") created the wrong mental model. He introduced "should" language and story-based acceptance criteria. Aslak Hellesoy created Cucumber in 2008 as a Gherkin-based BDD tool for Ruby, later ported to dozens of languages.

Examples

Cucumber feature file and step definitions in Ruby

# features/checkout.feature
# Feature: Shopping cart checkout
#   As a registered customer
#   I want to place an order
#   So that I receive the products I want
#
#   Scenario: Successful checkout with valid payment
#     Given I have 2 items in my cart totalling $59.98
#     And I have a valid credit card on file
#     When I confirm the order
#     Then the order status should be "confirmed"
#     And I should receive a confirmation email

# features/step_definitions/checkout_steps.rb
Given('I have {int} items in my cart totalling ${float}') do |count, total|
  @cart = Cart.new
  count.times { @cart.add_item(build(:item, price: total / count)) }
end

Given('I have a valid credit card on file') do
  @user = create(:user, :with_payment_method)
end

When('I confirm the order') do
  @order = CheckoutService.new.place(@cart, @user)
end

Then('the order status should be {string}') do |status|
  expect(@order.status).to eq(status)
end

The feature file is the source of truth and is readable by product managers. Step definitions are shared across scenarios; the same "I have N items" step works for any checkout scenario. Cucumber reports which scenarios passed, giving a living test report.

RSpec with subject and shared examples

# spec/models/subscription_spec.rb
RSpec.describe Subscription do
  subject(:subscription) { build(:subscription, plan: plan, status: :active) }

  shared_examples 'a cancellable subscription' do
    it 'transitions to cancelled' do
      subscription.cancel
      expect(subscription.status).to eq(:cancelled)
    end

    it 'sets cancelled_at timestamp' do
      freeze_time do
        subscription.cancel
        expect(subscription.cancelled_at).to eq(Time.current)
      end
    end
  end

  context 'with a monthly plan' do
    let(:plan) { build(:plan, interval: :monthly) }
    it_behaves_like 'a cancellable subscription'

    it 'bills at end of billing period' do
      expect(subscription.next_billing_date).to eq(1.month.from_now.to_date)
    end
  end

  context 'with an annual plan' do
    let(:plan) { build(:plan, interval: :annual) }
    it_behaves_like 'a cancellable subscription'
  end
end

shared_examples eliminates duplication: both monthly and annual subscriptions must be cancellable. Adding an annual-specific behaviour test does not duplicate the cancellation tests. freeze_time (timecop gem) removes time-dependent flakiness.

Use Cases

  • 01Acceptance testing for user-facing features where product owners need to verify that specifications are implemented correctly
  • 02Teams practising three amigos (developer, tester, product owner) refinement where scenarios are written collaboratively before development
  • 03Regulated industries (finance, healthcare) where auditors need human-readable proof that specified behaviours are tested and passing
  • 04API design documentation: BDD specs describe the behaviour of an API endpoint from the consumer's point of view

When Not to Use

  • //Internal utility code or infrastructure where business language is not relevant and Given/When/Then phrasing is forced
  • //Teams without product owner engagement; if specs are only read by developers, plain unit tests are less overhead
  • //Rapid prototyping where the behaviour is still being discovered; writing Gherkin scenarios for uncertain features wastes effort

Technical Notes

  • Cucumber's step definition regexp matching allows parameterised steps: {int}, {float}, {string}, {word} are built-in parameter types. Custom types can transform matched text into domain objects
  • RSpec's let is lazily evaluated: the block runs on first reference and is memoised for the example. let! forces eager evaluation. subject is the implied first argument to expect; subject(:name) aliases it
  • Page Object Model (POM) is a companion pattern for BDD UI tests: each page or component has a class that wraps Capybara/Selenium interactions, isolating selector fragility from the step definitions
  • Flaky BDD tests (inconsistent pass/fail) usually stem from time dependencies, order dependencies, or missing test database cleanup. DatabaseCleaner gem (Ruby) and Jest's --runInBand flag address common sources