Skip to content
behaviour-driven.org

Concept

A Behaviour Driven Programming Example, Worked Start to Finish

The mechanics of behaviour driven programming are easier to see on one feature carried the whole way through than in the abstract. This is a small one, deliberately.

The request as it arrived

Customers should not be able to overdraw their accounts.

This is a rule, not an example, and it contains at least three unanswered questions. Does an attempt fail silently or report something? Is a balance of exactly zero an overdraw? Does a pending transaction count against the balance?

The conversation

Three people spend fifteen minutes on it. The tester asks about exactly zero. The product owner says zero is fine, below zero is not. The developer asks about pending transactions and discovers that nobody had considered them, and that the answer is yes, they count. That answer did not exist before the meeting.

The examples that came out of it

Feature: Overdraw protection

  Scenario: A withdrawal that leaves the balance at zero
    Given the account balance is 20
    When the customer requests 20
    Then the machine dispenses 20
    And the account balance is 0

  Scenario: A withdrawal that would take the balance below zero
    Given the account balance is 20
    When the customer requests 30
    Then the machine dispenses nothing
    And the customer is told the balance is insufficient

  Scenario: A pending transaction counts against the balance
    Given the account balance is 100
    And a pending transaction of 90
    When the customer requests 20
    Then the machine dispenses nothing

Note what the wording avoids. No step mentions a screen, a button or a database table. The third scenario exists only because of a question asked in the conversation, which is the return on having held it.

The step definitions

Each line maps to a small piece of code. In Python with behave the shape is:

@given('the account balance is {amount:d}')
def step_balance(context, amount):
    context.account = Account(balance=amount)

@when('the customer requests {amount:d}')
def step_request(context, amount):
    context.result = context.machine.withdraw(context.account, amount)

@then('the account balance is {amount:d}')
def step_check_balance(context, amount):
    assert context.account.balance == amount

The scenario stays in the domain’s language. The step definition holds the mechanics. When the withdrawal moves from a method call to an HTTP request, only the middle layer changes and the specification is untouched, which is the property that makes the examples worth keeping.

The implementation

Written last, against the examples, in small steps with unit tests driving the internal design. The three scenarios fail first, for the right reasons, and pass once the rule including the pending-transaction case is in place.

The pending-transaction rule is the whole argument for the practice. It was not in the original request, it would not have been in the code, and it would have been found in production.

What the step definitions should not contain

The commonest mistake in this layer is putting logic in it. A step definition that calculates the expected outcome has reimplemented the system inside the test, and it will agree with the code even when both are wrong.

The Then step above asserts against a literal number that came out of a conversation. That literal is doing the work. Replacing it with account.expected_balance() would make the test pass under any implementation of the rule, including a broken one.

The second mistake is length. Where a step definition grows past a few lines it is usually performing setup that belongs in a helper, or driving an interface that should be wrapped. Long step definitions become untestable themselves, and they are the reason some teams end up maintaining a test suite for their test suite.

Why this example is small on purpose

Three scenarios and one rule looks slight next to a real feature, and the slightness is the point: it is roughly the amount of specification a fifteen-minute conversation produces, and that is the unit the practice actually operates on.

Teams that try to specify a whole release this way stall, because the conversation cannot hold that much and the resulting scenarios stop being readable. One rule, a handful of cases, including the case somebody thought of in the room. Then the next rule.

Frequently asked questions

What is a step definition?

The code that connects one line of a scenario to the system. The scenario stays in the domain language; the step definition holds the mechanics, and it is the only part that changes when the interface does.

Should step definitions contain assertions?

Then-steps do. Given and When steps should set up state and act, and a Given that asserts is usually hiding a missing scenario.

How much code should a step definition contain?

Very little. A step definition that runs to dozens of lines is doing work that belongs in the application or in a helper, and it becomes untestable itself.