8. BDD and Behave

Author:Peter Parente
Builds-on:Python

8.1. Goals

  • Define test driven development (TDD)
  • Define behavior driven development (BDD)
  • Understand the use cases for Gherkin
  • Understand the red-green-refactor cycle
  • Practice reading and writing spec with Gherkin
  • Practice BDD with Behave

8.2. Introduction

Behavior driven development (BDD) is a way of writing software. It starts with describing the behavior software in a story-like format. It then proceeds with developers doing the following repeatedly, what is known as the red-green-refactor cycle:

  • Writing a failing test case for a behavior (red)
  • Implementing the minimum code required for the test to pass (green)
  • Refactoring the code to meet project standards (refactor)

There are many libraries supporting behavior- or test-driven development: Cucumber for Ruby, Mocha for JavaScript, easyb for Groovy/Java, etc. In this session, we’ll learn using Behave for Python.

To get started, watch the TDD / BDD slidecast (~45 minutes) introducing test- and behavior-driven development using the Gherkin language for specification and Behave for testing. The slidecast includes a demo writing specs, tests, and code for the following:

If time permits, review these additional pages:

8.3. Exercises

You will need to complete the Setting Up instructions before you proceed with these exercises. Once you are set up, SSH into tottbox using the vagrant ssh command from the setup instructions. Then tackle the problems below. Document what you find in a gist and share it with the TotT community later.

8.3.1. Read the rules of RPSLK

Rock-Paper-Scissors-Lizard-SpocK is an extension of the classic rock-paper-scissors game. Read and understand the rules.

8.3.2. Gather requirements

Imagine I am paying you to implement a version of this game in Python. The game should declare a winner of the best of 5 rounds. In each round, the game should prompt the user to choose a gesture. The game should then generate a random gesture of its own and report the winner of the round, computer or human. If the round results in a tie, it should not count toward the 5 round limit and should be replayed. The game should announce the winner as soon as the victory is decided (e.g., the first player to win 3 rounds wins the game).

The game should work at the command line for now. Later, I might decide to port it to the web. I need you to design the game to make this port easy. I ask that you separate the game logic (model) from the CLI (view and controller). Two separate modules, model.py for the game logic and rpskl.py for the CLI the user runs, should do the trick.

8.3.3. Bootstrap the project structure

Create the following simple project layout on disk in new shared folder called rpslk:

rpslk/
    features/
        steps/
            model.py
            cli.py
        cli.feature
        environment.py
        model.feature
    model.py
    rpslk.py

Initialize a git repository there and git commit the empty files as a seed. Commit your work as you move through these exercises.

8.3.4. Write model.feature

Given the requirements above, write the Gherkin feature file for the game model including the scenarios below. Use the three completed scenarios, the examples in the Behave documentation, the feature tests you ran when setting up tottbox, and any other Gherkin examples you can find on the web as references. Don’t spend too much time on them now as you will tighten them up when you start writing the test code.

Feature: RPSLK game logic

    Scenario: User inputs a supported gesture RPSLK
        Given a user gesture
        When the game processes the round
        Then it returns the result of the round

    Scenario: User beats computer in a round
        # TODO

    Scenario: Computer beats user in a round
        # TODO

    @wip
    Scenario Outline: User and computer tie in a round
        Given the user gesture <user_gesture>
        And the computer gesture is the same
        When the game processes the round
        Then it reports the result as a "tie"

        Example: Gestures
            | gesture |
            | rock    |
            | paper   |
            | scissors|
            | lizard  |
            | spock   |

    Scenario: User wins the whole game
        Given the user has won 2 rounds
        And the user gesture is "rock"
        And the computer gesture is "scissors"
        When the game processes the round
        Then it indicates the user has won the game

    Scenario: Computer wins the whole game
        # TODO

Test the syntax of your feature file by doing the following on tottbox

cd /vagrant/rpslk
behave

The command should output your scenario text and mark each one failing because it is not yet implemented. It will also give (poor) code samples you can use to start implementing the test cases. Have a look at them and then move on. (I say poor because behave makes every test step explicit without considering test code reuse. Other libs are better at these suggestions.)

8.3.5. Test and implement one scenario

Add the following test code to your features/steps/model.py file. It completely implements the User and computer tie in a round scenario test case. Read the docstrings for each function to get an idea of what is going on.

from behave import given, when, then

@given(u'the user gesture {user_gesture}')
def step_impl(context, user_gesture):
    '''
    Store the user's gesture in the context for later steps.
    '''
    context.user_gesture = user_gesture


@given(u'the computer gesture is the same')
def step_impl(context):
    '''
    Dictate that the game Model instance must have a method named
    generate_gesture() that will return the random computer gesture for the
    round. Replace that method here with a function that returns the
    same gesture as the user gesture. This is called "mocking".
    '''
    context.model.generate_gesture = lambda: context.user_gesture


@when(u'the game processes the round')
def step_impl(context):
    '''
    Dictate that the game Model instance must have a method named
    process_round() that takes the user gesture for the round as a parameter.
    Save the return value in the context for later steps.
    '''
    context.result = context.model.process_round(context.user_gesture)


@then(u'it reports the result as a {result}')
def step_impl(context, result):
    '''
    Assert that the result of the round matches what the spec stated should
    happen.
    '''
    assert context.result == result

Notice that context.model is assumed to exist. That is, the test steps assume a game model is available for testing. We can ensure this is the case for each scenario by adding the following code to the features/environment.py file. (Yes, the environment.py file goes in features/ not in features/steps.)

from model import Model

def before_all(context):
    context.model = Model()

For this import to succeed, you must add a class named Model to the model.py file in the root of the project. Add the following empty class to that file.

class Model:
    pass

Now run behave in /vagrant/rsplk. Notice the lengthy output. Somewhere near the top you should see When the game processes the round in red ink and below that a stack trace indicating that the process_round() method is missing.

Welcome to the red-green-refactor cycle! You now have a red test. Your goal is to turn it green by fixing the implementation.

Implement the shell of the missing method and run behave -t @wip again. If you got the message signature right, that line of text should become green and the next one should show red. If not, the line will remain red but the stack trace will change. Continue in this fashion until the entire scenario is green. (Hint: Implement a generate_gesture() method for process_round() to invoke and the test to mock. Then add the game logic to compare the user and generated gesture in process_round().)

8.3.6. Learn about behave options

Have a look at behave --help. Investigate the use of tags such as @wip and the various formatting options of behave. Customize your future invocations of behave to suit your liking.

8.3.7. Test and implement the other scenarios

(Re)Using the above test steps, the Behave documentation, steps you ran to verify your tottbox setup, and examples you find on the web, test and implement the remaining scenarios. Work each one as a pair: first write the test code, then code the implementation, and then debug the test/implementation pair. When the test passes, move onto the next scenario, refactoring your game or test code when needed.

Don’t forget to move the @wip to the current scenario you’re working or remove it all together when you’re done.

8.3.8. Fill the gaps

Review your game model scenarios, tests, and implementations. Can you think of any other behaviors that your spec should capture or your test cases check? If so, spec, test, and implement them if you haven’t already. (Hint: Can anything go wrong?)

8.3.9. Spec, test and implement the CLI scenarios

At this point, you have an API for the RPSLK game, but you have no user interface. You need to implement the CLI. Write the scenarios, tests, and implementation for the CLI following the pattern you practiced for the game model. (Hint: Keep it simple.)

8.3.10. Document your experience

What are the pros and cons of behavior-driven development? Test-driven development? Gherkin? When might you follow this process to a T? When might you seek “shortcuts”? What are some alternative workflows you might envision?

8.4. Projects

If you want to try your hand at something larger than an exercise, consider one of the following.

8.4.1. Compare Behave with unittest

Look into the classic unittest package in the Python standard library. Try porting a few tests to it. What are the differences? When might you use one over the other? Write about it.

8.4.2. Port it to JavaScript and Mocha

Mocha is a highly popular test framework for JavaScript. It is a unique blend of specification and test implementation that “feels right” in JavaScript.

Port your specs, tests, and implementation from Behave and Python to Mocha and JavaScript. Document your experience. What’s different due to language? Library? Test philosophy? What’s the same?

8.5. References

Behave
Behavior-driven development, Python style
CucumberJS
A port of the Cucumber BDD library from Ruby to JavaScript
Mocha
Test-driven development, JavaScript style