Experiments with Behavior Driven Development

This is my introduction to behavior driven development (BDD) in Python. I have implemented a simple RESTful application using the Flask web framework. Lettuce is used enforce the mapping of user stories to tests via very simple description language called gherkin.

References:

My first user story written in Gherkin

Gherkin is the language that Lettuce understands. It is a Business Readable, Domain Specific Language that lets one describe software’s behaviour without detailing how that behaviour is implemented.

Lettuce is a Python tool for BDD that understands Gherkin. It can execute plain-text (Gherkin) functional descriptions as automated tests for Python projects.

Lettuce makes the development and testing process traceable, scalable, readable and - what is best - it allows someone who doesn’t program to describe the behavior of our system, without imagining those descriptions will automatically test the system during its development.

In this experiment, user stories are captured in user.features:


In [7]:
%%bash
cat test/features/user.features


Feature: RESTful server
    In order to play with BDD and REST
    As beginners
    We will handle storing, retrieving and deleting user details in a RESTful manner

    Scenario: Retrieve a user's details
        Given some users are in the system
        When I retrieve the user 'david01'
        Then I should get a '200' response
        And the following user details are returned:
            | name |
            | David |
    
    Scenario: Delete an existing user's details
        Given some users are in the system
        When I delete the user 'david01'
        Then I should get a '200' response
        And the message 'User david01 deleted' is returned
        And the user 'david01' is removed
        Then when I retrieve the user 'david01'
        Then I should get a '404' response
        And the message 'User david01 not found' is returned

Behavior test implementation

User features define behaviors, but Python code is required to test the behaviors. These are implemented in Python in file by convention named steps.py.

If one runs lettuce without implementing the step(s), lettuce will quite helpfully provide stubs that will execute but, at this point correctly, fail. Here is an example for the step And the user 'david01' is removed.

You can implement step definitions for undefined steps with these snippets:

# -*- coding: utf-8 -*-
from lettuce import step

@step(u'And the user \'([^\']*)\' is removed')
def and_the_user_group1_is_removed(step, group1):
    assert False, 'This step must be implemented'

Here are the behavior tests for user queries and deletes.


In [28]:
%%bash
pygmentize ./test/features/steps.py


# -*- coding: utf-8 -*-

'''
Allow defining steps and store values to be used across each step in the
world object.
'''
from lettuce import step, world, before
from nose.tools import assert_equals
from app.application import app, USERS
import json

# Test user query
@before.all
def before_all():
    '''Setup for testing.'''
    world.app = app.test_client()

@step(u'Given some users are in the system')
def given_some_users_are_in_the_system(step):
    '''Add a user to USERS for testing against.'''
    USERS.update({'david01': {'name': 'David'}})

@step(u'When I retrieve the user \'(.*)\'')
def when_i_retrieve_the_user_group1(step, username):
    ''' A capture group is used in the regular expression allowing us to pass
    in variables to the step. This allows for the reuse of steps.'''
    world.response = world.app.get('/users/{}'.format(username))

@step(u'Then I should get a \'(.*)\' response')
def then_i_should_get_a_group1_response_group2(step, expected_status_code):
    '''Check the status_code.'''
    assert_equals(world.response.status_code, int(expected_status_code))

@step(u'And the following user details are returned:')
def and_the_following_user_details(step):
    '''Check the correct user data is in the response.'''
    assert_equals(step.hashes, [json.loads(world.response.data)])


# Test existing user deletion
@step(u'When I delete the user \'([^\']*)\'')
def when_i_delete_the_user_group1(step, username):
    world.response = world.app.delete('/users/{}'.format(username))

@step(u'And the message \'([^\']*)\' is returned')
def and_the_message_group1_is_returned(step, group1):
    assert_equals(world.response.data, group1)

@step(u'And the user \'([^\']*)\' is removed')
def and_the_user_group1_is_removed(step, group1):
    world.response = world.app.get('/users/{}'.format(group1))
    assert_equals(world.response.status_code, int(404))

Running the REST server manually with curl

GET


In [8]:
%%bash 
curl --silent localhost:5000/


Welcome to my Behavior Driven Development REST server

In [9]:
%%bash 
curl --silent localhost:5000/users


{
  "jack01": {
    "name": "Jack"
  }, 
  "seth02": {
    "name": "Seth"
  }, 
  "zero00": {
    "name": "Zero"
  }
}

In [22]:
%%bash 
curl --silent localhost:5000/users/zero00


{
  "name": "Zero"
}

DELETE


In [13]:
%%bash
curl -i --silent -H "Accept: application/json"  http://127.0.0.1:5000/users/zero00


HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 20
Server: Werkzeug/0.11.2 Python/2.7.6
Date: Mon, 14 Dec 2015 06:18:18 GMT

{
  "name": "Zero"
}

In [15]:
%%bash
curl -i --silent -H "Accept: application/json" -X DELETE http://127.0.0.1:5000/users/zero00


HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 19
Server: Werkzeug/0.11.2 Python/2.7.6
Date: Mon, 14 Dec 2015 06:20:53 GMT

User zero00 deleted

In [16]:
%%bash
curl -i --silent -H "Accept: application/json"  http://127.0.0.1:5000/users/zero00


HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 21
Server: Werkzeug/0.11.2 Python/2.7.6
Date: Mon, 14 Dec 2015 06:21:11 GMT

User zero00 not found

Automated tests using lettuce


In [20]:
%%bash
lettuce test/features/user.features
# Excuse the double printing. This does not happen on the CLI.


Feature: RESTful server                                                            # test/features/user.features:1
  In order to play with BDD and REST                                               # test/features/user.features:2
  As beginners                                                                     # test/features/user.features:3
  We will handle storing, retrieving and deleting user details in a RESTful manner # test/features/user.features:4

  Scenario: Retrieve a user's details                                              # test/features/user.features:6
    Given some users are in the system                                             # test/features/steps.py:19
    Given some users are in the system                                             # test/features/steps.py:19
    When I retrieve the user 'david01'                                             # test/features/steps.py:24
    When I retrieve the user 'david01'                                             # test/features/steps.py:24
    Then I should get a '200' response                                             # test/features/steps.py:32
    Then I should get a '200' response                                             # test/features/steps.py:32
    And the following user details are returned:                                   # test/features/steps.py:37
      | name  |
      | David |
    And the following user details are returned:                                   # test/features/steps.py:37
      | name  |
      | David |

  Scenario: Delete an existing user's details                                      # test/features/user.features:14
    Given some users are in the system                                             # test/features/steps.py:19
    Given some users are in the system                                             # test/features/steps.py:19
    When I delete the user 'david01'                                               # test/features/steps.py:44
    When I delete the user 'david01'                                               # test/features/steps.py:44
    Then I should get a '200' response                                             # test/features/steps.py:32
    Then I should get a '200' response                                             # test/features/steps.py:32
    And the message 'User david01 deleted' is returned                             # test/features/steps.py:48
    And the message 'User david01 deleted' is returned                             # test/features/steps.py:48
    And the user 'david01' is removed                                              # test/features/steps.py:52
    And the user 'david01' is removed                                              # test/features/steps.py:52
    Then when I retrieve the user 'david01'                                        # test/features/steps.py:24
    Then when I retrieve the user 'david01'                                        # test/features/steps.py:24
    Then I should get a '404' response                                             # test/features/steps.py:32
    Then I should get a '404' response                                             # test/features/steps.py:32
    And the message 'User david01 not found' is returned                           # test/features/steps.py:48
    And the message 'User david01 not found' is returned                           # test/features/steps.py:48

1 feature (1 passed)
2 scenarios (2 passed)
12 steps (12 passed)

TODO

Test:

  • Update user data