Profile Photo

Jamie Skipworth


Technology Generalist | Software & Data


Behaviour Driven Development - BDD

Sometimes you take a day or two off work and come back having missed some major development in, er, development. A while ago I was told I was doing testing wrong; test-driven development wasn’t good enough, it has to be behaviour driven. Easy mistake to make, I guess.

So, the aim of this little nugget of gold in blog-post form is to briefly describe what behaviour-driven development (BDD) is, how you’d use it and what it looks like. So, here we go.

The What

So what is it? Before I answer that it’s worth quickly describing TDD, where you would first think about what your code needs to do, and then develop tests for it first. The idea is that if you develop your code with tests in mind, you’ll produce less buggy code more quickly - the tests inform your development, and I suppose make the codebase easier to reason about. In the end each function you write will have an associated test.

With BDD you take a step back and think more in terms user behaviour instead of the lower-level nuts and bolts. With BDD a single test scenario might chain several steps together. Web applications are great examples of where this approach works very well, where you might, for example, test a password-reset process, product ordering, or subscription to a mailing list (ha!). These are examples of where lots of different bits of code are being run to achieve a higher-level outcome.

The How

There are tons of different libraries for BDD out there. I’m going to use one called behave for Python (Python 3, because get with the times). The principles remain the same regardless of the language you use. It’s all just syntax, unless you’re using Maven with Java in which case may $DEITY help you.

All the code I’m going to use can be found on GitHub here. It is quick and dirty, possibly over-complicated and probably incomplete, but the goal is to demo BDD.

Features

The first thing you need to know about BDD is that it is driven by features (also sometimes called stories). Features define one or more scenarios that make up the feature. These are written in a structured English-like language to make them easy to write and understand. Gherkin is a well-known language which is used by the Cucumber BDD framework.

Lets say we’re selling frozen bananas from a Banana Stand. We want the stand to be able to track total sales of bananas (a feature), so a simple example of a BDD test might look like this:

Feature: Keep track of total sales

  Scenario: sell some bananas
    Given we have have a banana stand with inventory
        | items   | qty |
        | bananas | 10  |
        | nuts    | 10  |
        | choc    | 10  |
      When we sell 5 frozen bananas at 4.50 each 
      Then we will have total sales of 22.50

It’s pretty clear what this is going to test, isn’t it? And that’s the point. We could have written more scenarios under this feature to test things like what happens when we run out of inventory, but you get the idea.

The most important elements of this feature are the keywords Given, When, and Then. These define some pre-conditions (Given), some event that has to occur (When), and what outcome we’re expecting (Then). You can use the And keyword to chain a few of these together if needed. Also notice how we’ve included some data into this feature - the list of inventory, and the number of bananas to sell and at what price. Review the syntax for more info on how to write features.

So far, so simple. But how to we get from this high-level feature to actually testing it?

Steps

Steps are the code that interprets the features and actually implements the logic. Essentially what happens is pattern/regex matching for the given/when/then expressions in the feature definition. Each given, when and then statement should have a respective @given, @when and @then-decorated function.

Once we’ve written our banana stand code, we might write steps like this:

from behave import *
from stand import *

frozen_banana_recipe = { "bananas": 1, "nuts": 1, "choc": 1 } #Yum!

@given(u'we have have a banana stand with inventory')
def step_impl(context):
    inventory = {}
    for row in context.table:
        inventory.setdefault( row['items'], int(row['qty']) )

    context.banana_stand = Stand( inventory_from_dict=inventory )
    context.initial_inventory = inventory

@when(u'we sell {qty:d} frozen bananas at {price:f} each')
def step_impl(context, qty, price ):
    for i in range( qty ):
        context.banana_stand.sell_product( frozen_banana_recipe, price )

@then(u'we will have total sales of {sales:f}')
def step_impl(context, sales):
    assert sales == context.banana_stand.total_sales

@then(u'we will have total quantity sold of {qty:d}')
def step_impl(context, qty):
    assert qty == context.banana_stand.total_products_sold

Our Given statement executes the method decorated with @given. This triggers the creation of our Stand object and populates it with our inventory. Note the context parameter - this is built-in to behave and allows us to extract data from the scenario and share state between methods. Here we’re extracting the inventory list, and using it to initialise the Stand object. We then also store it in initial_inventory so we can remember what we started with.

Further down in our @when function extracts some parameters from the scenario. This time it’s the number of bananas to sell and at what price. We pass these along to our Stand object into sell_product. These are parameters because we may want to test it with weird values later like negatives, floats, etc.

Finally, in @then we test to see if the sales figure has been calculated correctly based on the number of sales and the price, using an assertion.

The Banana Stand

So we have an idea of what our banana stand should do. Here are some features and scenarios.

Feature: Keep track of total sales

  • Scenario: sell some bananas
  • Scenario: sell some bananas without enough stock

Feature: Keep track of inventory

  • Scenario: add items to inventory

Feature: Keep track of quantity of bananas sold

  • Scenario: sell some bananas
  • Scenario: sell some bananas without enough stock

With behave features must be stored in a features sub-directory (one feature per file), with another steps subdirectory under that. Like this:

.
├── features
│   ├── inventory.feature
│   ├── qty.feature
│   ├── sales.feature
│   └── steps
│       ├── bananastand.py
│       └── inventory.py
└── stand.py

With the code written (stand.py) and all the features and steps in the correct place, all we need to do now is execute the tests. This is as simple as running behave in the project root.

(bdd_bananas) $ behave
Feature: Keep track of inventory # features/inventory.feature:1

  Scenario: add items to inventory                   # features/inventory.feature:3
    Given we have have a banana stand with inventory # features/steps/bananastand.py:6 0.000s
      | items   | qty |
      | bananas | 10  |
      | nuts    | 10  |
      | choc    | 10  |
    Then the inventory will match                    # features/steps/inventory.py:4 0.000s
      | items   | qty |
      | bananas | 10  |
      | nuts    | 10  |
      | choc    | 10  |

Feature: Keep track of quantity of bananas sold # features/qty.feature:1

  Scenario: sell some bananas                        # features/qty.feature:3
    Given we have have a banana stand with inventory # features/steps/bananastand.py:6 0.000s
      | items   | qty |
      | bananas | 10  |
      | nuts    | 10  |
      | choc    | 10  |
    When we sell 5 frozen bananas at 4.50 each       # features/steps/bananastand.py:15 0.000s
    Then we will have total quantity sold of 5       # features/steps/bananastand.py:25 0.000s

  Scenario: sell some bananas without enough stock   # features/qty.feature:12
    Given we have have a banana stand with inventory # features/steps/bananastand.py:6 0.000s
      | items   | qty |
      | bananas | 1   |
      | nuts    | 1   |
      | choc    | 1   |
    When we sell 5 frozen bananas at 4.50 each       # features/steps/bananastand.py:15 0.000s
    Then we will have total quantity sold of 1       # features/steps/bananastand.py:25 0.000s

Feature: Keep track of total sales # features/sales.feature:1

  Scenario: sell some bananas                        # features/sales.feature:3
    Given we have have a banana stand with inventory # features/steps/bananastand.py:6 0.000s
      | items   | qty |
      | bananas | 10  |
      | nuts    | 10  |
      | choc    | 10  |
    When we sell 5 frozen bananas at 4.50 each       # features/steps/bananastand.py:15 0.000s
    Then we will have total sales of 22.50           # features/steps/bananastand.py:21 0.000s

  Scenario: sell some bananas without enough stock   # features/sales.feature:12
    Given we have have a banana stand with inventory # features/steps/bananastand.py:6 0.000s
      | items   | qty |
      | bananas | 1   |
      | nuts    | 1   |
      | choc    | 1   |
    When we sell 5 frozen bananas at 4.50 each       # features/steps/bananastand.py:15 0.000s
    Then we will have total sales of 4.50            # features/steps/bananastand.py:21 0.000s

3 features passed, 0 failed, 0 skipped
5 scenarios passed, 0 failed, 0 skipped
14 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.002s
(bdd_bananas) $ 

Conclusion

So there you have it. We’ve successfully used BDD to test that the banana stand works as expected given certain user behaviour. And it wasn’t that painful! Give BDD a go next time you’re testing code that has complex interactions. It’ll save you a lot of anguish.

All code here can be found here: https://github.com/scrollocks/bdd_bananas