Behavior Driven Development with Lettuce¶
Note
The code built in this short demonstration is available as
fizzbuzz.tgz
Let’s imagine that the whiteboard exercise we did previously, FizzBuzz, was actually a project we had to implement for a client.
In order to ensure that the work we did for that project fulfilled the client’s requirements, we would definitely want to test the code. But unit tests–and the output they produce–are not the easiest things for clients to read.
So instead, you decide to use this opportunity to use We’re going to use the Python package lettuce to incorporate behavior driven development. That way, the tests will be clear demonstrations of the business value of your code to your client.
Practice Safe Development¶
Because this is a client project, you’ll set up a virtualenv project for it as you always do.
Install Lettuce¶
Then you’ll install lettuce
:
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ pip install lettuce
Downloading/unpacking lettuce
...
Successfully installed lettuce sure fuzzywuzzy nose rednose python-termstyle
Cleaning up...
[fizzbuzz]
[master=]
Once that’s finished, we should find that we have a new command available in
our shell: lettuce
:
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ which lettuce
/Users/cewing/virtualenvs/fizzbuzz/bin/lettuce
After this, make sure to use pip freeze > requirements.txt
to dump your
requirements out to a file for easy replication.
At this point, your project directory would look something like this:
fizzbuzz/
├── LICENSE
├── README.rst
├── fizzbuzz.py
└── requirements.txt
Since you haven’t actually written any code for the project yet, your
fizzbuzz.py
file might look like this:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""The module provides two functions, fizzbuzz and fizzbuzz_extended
"""
def fizzbuzz(n):
"""return a fizzbuzz-formatted representation of n"""
pass
Working With Lettuce¶
There’s a nice walkthrough for lettuce on the website. If you’re starting a project totally from scratch, try it out.
Getting Started¶
The basic gist of the walkthrough tells us that lettuce
works by
convention. To have lettuce tests, you have to set up an appropriate directory
structure in your Python project.
We’ll create a directory called features
. And in it we’ll create two
files: fizzbuzz.feature
and steps.py
.
The <name>.feature
file is where we’ll create our BDD tests. The
name of the file is arbitrary, the extension is not.
The <name>.py
file is where we will create the Python code that
implements steps in our BDD tests. Again, the name is arbitrary, and
lettuce
will scan for any .py
files that contain step
definitions.
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ mkdir features
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ touch features/fizzbuzz.feature
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ touch features/steps.py
Once you create the files you’ll need, your directory structure will look something like this:
fizzbuzz/
├── README.rst
├── features
│ ├── fizzbuzz.feature
│ └── steps.py
├── fizzbuzz.py
└── requirements.txt
Write a Scenario¶
If we wanted to express the fizzbuzz game as a user story, it might go something like this:
As a user, when I call fizzbuzz with the number 5, I see 'Buzz'.
Expressed in BDD form, this translates to something like this:
Given the number 5, when I call fizzbuzz, then I see the output 'Buzz'
Let’s add this ‘Scenario’ to our BDD tests:
Feature: Simple FizzBuzz
Implement a simple version of the FizzBuzz game
Scenario: FizzBuzz of 5
Given the number 5
When I call FizzBuzz
Then I see the output Buzz
Implement The Steps¶
We have three steps. Lettuce will match our steps to Python functions using the text we set in the scenario, so we need to create three steps.
Steps are defined as Python functions decorated with the @step
decorator
factory from lettuce. The decorator factory takes a single argument which is
the text used for the step in scenarios. Variable values are matched by regular
expression.
Let’s implement the first step first. In steps.py
add the following:
from lettuce import step
from lettuce import world
from fizzbuzz import fizzbuzz
@step('the number (\d+)')
def the_number(step, num):
world.number = int(num)
Here we can see that the @step
decorator is passed the argument “the number
(d+)”. How does that match? The secret is that there are a few ‘magic’ words
in lettuce
. When the test parser sees Given
, When
or Then
it
uses these to determine that the following text on the line is a step. That
text is what is considered for the match.
The regular expression (\d+)
will match a sequence of one or more digits,
and the value it finds will be passed to the function decorated by @step
.
Also notice that the Python function decorated by @step
takes two
arguments. The first will always be the step itself. The second comes from the
matched regular expression.
How do you expect you might handle a step like this:
Given the numbers 5 and 7
What would the argument passed to the @step
decorator look like? How many
arguments would the Python function need to expect?
Go ahead and add two more steps for the remaining parts of the scenario:
@step('when I call fizzbuzz')
def call_fizzbuzz(step):
world.fb = fizzbuzz(world.number)
@step('I see the output (\w+)')
def compare(step, expected):
assert world.fb == expected, "Got %s" % expected
Notice that the world
object provided by lettuce
allows passing values
from one function context to another.
Run Your Tests - Red¶
Once you’ve done this, you should be able to run your BDD tests from the command line:
[fizzbuzz]
[master *=]
heffalump:fizzbuzz cewing$ lettuce
Feature: Simple FizzBuzz # features/fizzbuzz.feature:1
Implement a simple version of the FizzBuzz game # features/fizzbuzz.feature:2
Scenario: FizzBuzz of 5 # features/fizzbuzz.feature:4
Given the number 5 # features/steps.py:8
When I call FizzBuzz # features/steps.py:13
Then I see the output Buzz # features/steps.py:18
Traceback (most recent call last):
...
AssertionError: Got None
1 feature (0 passed)
1 scenario (0 passed)
3 steps (1 failed, 2 passed)
You can see, the tests report that we’ve run 1 feature, 1 scenario and 3 steps. They also tell us that one of our steps failed. And they kindly provide a traceback that shows us where in our steps things went awry.
Implement Your Code¶
Now, back in fizzbuzz.py
implement your fizzbuzz function:
def fizzbuzz(n):
"""return a fizzbuzz-formatted representation of n"""
if n == 0:
return str(0)
out = ''
data = [(3, 'Fizz'), (5, 'Buzz')]
for divisor, replacement in data:
if not n % divisor:
out += replacement
if not out:
out = str(n)
return out
Run Your Tests - Green¶
Now you can re-run your test:
[fizzbuzz]
[master *=]
heffalump:fizzbuzz cewing$ lettuce
Feature: Simple FizzBuzz # features/fizzbuzz.feature:1
Implement a simple version of the FizzBuzz game # features/fizzbuzz.feature:2
Scenario: FizzBuzz of 5 # features/fizzbuzz.feature:4
Given the number 5 # features/steps.py:8
When I call FizzBuzz # features/steps.py:13
Then I see the output Buzz # features/steps.py:18
1 feature (1 passed)
1 scenario (1 passed)
3 steps (3 passed)
Iterating Over Multiple Calls¶
But it really isn’t enough to just test with one value. FizzBuzz is a dynamic process that returns different values depending on what you pass in. We should cover at least a reasonable range of alternative possibilities.
We could write a new scenario for each different value we want to pass in. But that would mean re-writing the same lines over and over with only two variations. Not very programmerish.
Luckily, there’s a solution. With the syntax supported by lettuce
we
can replace the specific numbers in our current scenario with
placeholders. Then we can provide a set of data for the scenario to use,
and lettuce
will automatically plug in our values and run the same
scenario over and over for us.
A Scenario Outline¶
Update fizzbuzz.feature
like so:
Scenario Outline: FizzBuzz [just enough]
Given the number <input>
When I call FizzBuzz
Then I see the output <output>
Examples:
| input | output |
| 0 | 0 |
| 1 | 1 |
| 3 | Fizz |
| 5 | Buzz |
| 6 | Fizz |
| 10 | Buzz |
| 15 | FizzBuzz |
We’ve changed our scenario into a Scenario Outline. This lets
lettuce
know that we expect this scenario to be run multiple times.
We also replaced the specific input 5
and output Buzz
with
placeholders, marked by angle brackets.
Finally, we provided a table of example data for our outline to use. The first row of the table contains the names of our placeholders, and then each row represents a set of values to be used for one iteration.
Run Your Tests - Refactor¶
Now when we run the tests again, we can see that we get more passes through the scenario automatically:
[fizzbuzz]
[master=]
heffalump:fizzbuzz cewing$ lettuce
Feature: Simple FizzBuzz # features/fizzbuzz.feature:1
Implement a simple version of the FizzBuzz game # features/fizzbuzz.feature:2
Scenario Outline: FizzBuzz [just enough] # features/fizzbuzz.feature:4
Given the number <input> # features/steps.py:8
When I call FizzBuzz # features/steps.py:13
Then I see the output <output> # features/steps.py:18
Examples:
| input | output |
| 0 | 0 |
...
1 feature (1 passed)
7 scenarios (7 passed)
21 steps (21 passed)
Next Steps¶
We’ve written one scenario that covers our simple implementation of FizzBuzz. But the assignment had a second implementation. That version was supposed to be extensible with additional numbers and the values that should be printed in their place. For example, if the number 7 was supplied, the value ‘Sizz’ should be returned.
You may also wish to read more of the documentation for lettuce
and
see if you can’t figure out how to add a new scenario and new steps that
will cover that version of the game.
To complete this task you’ll need to learn a bit more about the syntax of
the language you are using to write these tests. It’s called Gherkin
(the first implementation was called cucumber
). There is a very nice
outline of the features of Gherkin syntax you can read to learn more.
You should be aware that not all the features of the Gherkin
syntax
are supported by lettuce.
Moreover, some features are supported in
alternate forms, for example, Gherkin
backgrounds are implemented as
hooks in lettuce
.
Happy testing!