BDD4Django Tutorial

BDD is a software development process based on TDD (Test Driven Development), IMHO it turns the TDD process much more intuitive, allowing better workflow and high quality software.

In this post I’m gonna talk about the BDD4Django, a package for Python/Django that I developed to use BDD in your project seamless.

This package unifies 4 elements:

  • Django – The famous web framework for Python.
  • Morelia Viridis (with added features and better Django integration) – A fantastic BDD tool for Python.
  • Splinter – A tool that allows on-browser testing in Python.
  • Some TestCase classes that support out-of-the-box many kinds of BDD steps.

First, from the beginning.

Morelia is a BDD framework based on Ruby’s Cucumber style.
You write a .feature file that follows an ordinary English style(Given-When-Then) like below:


Feature: Calculator
 Scenario: Sum two numbers
 Given the number 10 and the number 20
 When I sum them
 Then I get 30

Scenario: Multiply and sum numbers
 Given the number 10 and the number 2
 When I multiply them
 And I sum 50 to the result
 Then I get 70

Splinter is an on-browser testing framework, with it you can launch and interact with the browser, like click in buttons, fill form fields, check presence of elements and text displaying in a browser, etc.

BDD4Django is a Morelia fork, so you don’t need to install Morelia if you’ve BDD4Django installed.
To install Splinter you can follow the instructions here: http://splinter.cobrateam.info/docs/install.html (ok, all you need is to use the good friend pip)

BDD4Django requires Django 1.3+, but if it’s a Django < 1.4 you need to install django-live-server https://github.com/adamcharnock/django-live-server (because the tests run on a live server)

All set then we can start to code!
BDD4Django has 2 main classes that you can extend from: BDDTestCase or BDDCoreTestCase.
The first one is for on-browser testing, the second one is for core tests, with no browser interaction.

In this post I’m going to talk about the BDDCoreTestCase, in the next posts I’ll address the BDDTestCase.

First, install the BDD4Django, to do that, simply type in your terminal:
pip install bdd4django
Then write your Django app, to follow this tutorial create a calculator app with the command:

manage.py startapp calculator

Then, add the app in the INSTALLED_APPS variable settings.
First things first, create a .feature file to write the scenarios, let’s begin with a scenario of a simple sum of two numbers in our calculator:

Feature: Math functions
Scenario: Sum two numbers
 When I call method "calculator.utils.sum" with params "{'x':10,'y':5}"
 Then I get the return "15"

Note that the method name comes after the module name (the app in this case), and the parameters use a dictionary-like syntax.

Now you need to write the tests.py file like the example bellow:


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

from bdd4django.helpers import BDDCoreTestCase

class MyTestCase(BDDCoreTestCase):
   def test_evaluate_file(self):
      self.parse_feature_file( 'calculator' )

Just create a class that extends from BDDCoreTestCase, and implements the test_evaluate_file calling the parse_feature_file method.
The parse_feature_file receives the app name as the first argument (the .feature file must be in the app directory root and be named as <app_name>.feature, if not you need to pass the argument file_path with the full path relative to the project root.
We created a calculator.feature file in the root directory, so just use the ‘calculator’ as argument.

Run the tests (python manage.py test calculator), and you’ll get the output:

File "../apps/calculator/calculator.feature", line 3, in \nScenario: Sum two numbers
 When: I call method "calculator.utils.sum" with params "{'x':10,'y':5}"\n
'module' object has no attribute 'sum'

Of course we get this error, we didn’t implement the utls.sum method, so let’s do that.
Create a file named utils.py and write in this:


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

def sum(x, y):
   return x + y

Now execute the tests again and you should get the result:


**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':10,'y':5}"
Step: I get the return "15"
.
----------------------------------------------------------------------
Ran 1 test in 0.045s

OK

Congratulations, you got the first success test executed.

Test only one set of parameters is a poor way of testing, and write one scenario for each set of parameters can be boring, so you can write only a single scenario with different set of variables, just change the calculator.feature this way:


Feature: Math functions
Scenario: Sum two numbers
 When I call method "calculator.utils.sum" with params "{'x':<x>,'y':<y>}"
 Then I get the return "<result>"

| x      | y      | result  |
| 10     | 5      |  15     |
| 99     | 1      |  100    |
| 3      | 978    |  981    |
| 0      | 0      |   0     |
| -5     | 4      |  -1     |
| -10    | -25    |  -35    |

For each scenario executed the test’ll replace the placeholders <x>, <y> and <result> for their respective values in a row of the table and you can see it in the output of test as seen bellow:


**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':10,'y':5}"
Step: I get the return "15"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':99,'y':1}"
Step: I get the return "100"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':3,'y':978}"
Step: I get the return "981"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':0,'y':0}"
Step: I get the return "0"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-5,'y':4}"
Step: I get the return "-1"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-10,'y':-25}"
Step: I get the return "-35"
.
----------------------------------------------------------------------
Ran 1 test in 0.060s

OK

But if you wanna load the values before call the method?
Simply implement the following scenario:


Scenario: Load before sum
 When I load value "123" in "self.value_x"
 And I load value "17" in "self.value_y"
 And I call method "calculator.utils.sum" with params "{'x':int(self.value_x),'y':int(self.value_y)}"
 Then I get the return "140"

The output should be something like this:


**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':10,'y':5}"
Step: I get the return "15"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':99,'y':1}"
Step: I get the return "100"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':3,'y':978}"
Step: I get the return "981"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':0,'y':0}"
Step: I get the return "0"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-5,'y':4}"
Step: I get the return "-1"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-10,'y':-25}"
Step: I get the return "-35"

**** Scenario: Load before sum
Step: I load value "123" in "self.value_x"
Step: I load value "17" in "self.value_y"
Step: I call method "calculator.utils.sum" with params "{'x':int(self.value_x),'y':int(self.value_y)}"
Step: I get the return "140"
.
----------------------------------------------------------------------
Ran 1 test in 0.082s

OK

As you can see all the tests are being executed, but if you want to execute only the last scenario?
So all you need is love add a scenarios parameter in the parse_feature_file call (tests.py) like this.


def test_evaluate_file(self):
   self.parse_feature_file( 'calculator',scenarios=('Load before sum',) )

The scenarios parameter is a tuple of scenario names, in the example we’ll execute only the last scenario.

The BDD4Django alerts you about the ignored scenarios like this:


!!!! Ignoring scenario: Sum two numbers

Let’s say that I want to save the values and result in the database, so I can create a very simple model like this one and sync the database:

from django.db import models

class Sum(models.Model):

   x = models.IntegerField('X')
   y = models.IntegerField('Y')
   result = models.IntegerField('Result')

And to check if the object was saved in the database I change the last scenario in the calculator.feature file:

Scenario: Load before sum
 When I load value "123" in "self.value_x"
 And I load value "17" in "self.value_y"
 And I call method "calculator.utils.sum" with params "{'x':int(self.value_x),'y':int(self.value_y)}"
 Then I get the return "140"
 And I see an object "calculator.models.Sum" with values "{'x':123,'y':17,'result':140}"

Run again the tests and you’ll get the output:

AssertionError:
 File "../apps/calculator/calculator.feature", line 19, in \nScenario: Load before sum
 And: I see an object "calculator.models.Sum" with values "{'x':123,'y':17,'result':140}"\n
0 not greater than 0

We didn’t change the sum method, so let’s do that:

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

from models import Sum

def sum(x, y):
   result = x+y

   Sum.objects.create( x=x, y=y, result=result )

   return result

Run the tests again… and you’ll see:

!!!! Ignoring scenario: Sum two numbers

**** Scenario: Load before sum
Step: I load value "123" in "self.value_x"
Step: I load value "17" in "self.value_y"
Step: I call method "calculator.utils.sum" with params "{'x':int(self.value_x),'y':int(self.value_y)}"
Step: I get the return "140"
Step: I see an object "calculator.models.Sum" with values "{'x':123,'y':17,'result':140}"
.
----------------------------------------------------------------------
Ran 1 test in 0.052s

OK

Everything is fine!

The sum method is pretty fine, how about to move to test the view?

First of all, implement the views.py:

# *-* coding: utf-8 *-*
from django.shortcuts import render_to_response
from django.template import RequestContext

def sum(request):

    x = 0
    y = 0

    if request.method == 'POST':
        x = int(request.POST.get('x'))
        y = int(request.POST.get('y'))

    return render_to_response(
        'calculator.html',
        {
            'result': x+y,
        },context_instance=RequestContext(request)
    )

This view receives 2 parameters via POST method (x and y) and render to a template called “calculator.html” the result of the sum of theses parameters.
Now we’ve to implement the template file.

Here’s a simple implementation of the calculator.html

{% load i18n %}

<html>
<head>
    <title>{% trans "Calculator" %}</title>
</head>
<body>

{% trans "Result: " %}{{ result }}

</body>
</html>

Add this line in yours urls.py to address the correct view.

url(r'^sum/$', 'calculator.views.sum', name='sum'),

To test views we need to override the extra_setup method of the BDDCoreTestCase, and initialize two properties (self.client and self.response).
self.client is the fake HTTP client of the TestCase class, and self.response stores the response of a HTTP request.

Here’s is the new MyTestCase class:

class MyTestCase(BDDCoreTestCase):

    def extra_setup(self):
        self.client = self.client_class()
        self.response = None

    def test_evaluate_file(self):
        self.parse_feature_file( 'calculator' )

Finally here’s the new scenario testing the sum via the view method:

 Scenario: Sum via view
   When I call view "sum" with post data "{'x':7,'y':35}"
   Then I get the template "calculator.html" rendered

You can change the parse_feature_file call to run only this last scenario:

self.parse_feature_file( 'calculator',scenarios=('Sum via view',) )

Run the tests, the output should be something like this:

!!!! Ignoring scenario: Sum two numbers

!!!! Ignoring scenario: Load before sum

**** Scenario: Sum via view
Step: I call view "sum" with post data "{'x':7,'y':35}"
Step: I get the template "calculator.html" rendered
.
----------------------------------------------------------------------
Ran 1 test in 0.315s

OK

Plus, I can test if the context variable result is with the correct value:

     ...
     And I get the context variables "result" with values "42"

And I can check if the result is correct displayed in the template

     ...
     And I see the text "Result: 42" in template

My final scenario looks like this:

Scenario: Sum via view
   When I call view "sum" with post data "{'x':7,'y':35}"
   Then I get the template "calculator.html" rendered
    And I get the context variables "result" with values "42"
    And I see the text "Result: 42" in template

And the output of this scenario test running is this one:

!!!! Ignoring scenario: Sum two numbers

!!!! Ignoring scenario: Load before sum

**** Scenario: Sum via view
Step: I call view "sum" with post data "{'x':7,'y':35}"
Step: I get the template "calculator.html" rendered
Step: I get the context variables "result" with values "42"
Step: I see the text "Result: 42" in template
.
----------------------------------------------------------------------
Ran 1 test in 0.320s

OK
Destroying test database for alias 'default'...

What about run all the tests together?
Remove the scenarios parameter from parse_feature_file call in the tests.py and run tests again.

Creating test database for alias 'default'...

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':10,'y':5}"
Step: I get the return "15"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':99,'y':1}"
Step: I get the return "100"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':3,'y':978}"
Step: I get the return "981"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':0,'y':0}"
Step: I get the return "0"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-5,'y':4}"
Step: I get the return "-1"

**** Scenario: Sum two numbers
Step: I call method "calculator.utils.sum" with params "{'x':-10,'y':-25}"
Step: I get the return "-35"

**** Scenario: Load before sum
Step: I load value "123" in "self.value_x"
Step: I load value "17" in "self.value_y"
Step: I call method "calculator.utils.sum" with params "{'x':int(self.value_x),'y':int(self.value_y)}"
Step: I get the return "140"
Step: I see an object "calculator.models.Sum" with values "{'x':123,'y':17,'result':140}"

**** Scenario: Sum via view
Step: I call view "sum" with post data "{'x':7,'y':35}"
Step: I get the template "calculator.html" rendered
Step: I get the context variables "result" with values "42"
Step: I see the text "Result: 42" in template
.
----------------------------------------------------------------------
Ran 1 test in 0.370s

OK

There are some variants of the methods present in this tutorial (like call view with parameters, with get data, call view logged as user and others), you can see the fully documentation for BDD4Django in my github: https://github.com/danielfranca/BDD4Django

Next post I’m going to talk about the BDDTestCase class and how to run your integration tests directly on browser.

I hope this tutorial helped you to embrace BDD and to understand BDD4Django, feel free to contact me about any doubts, suggestions or to criticize me in daniel.franca [at] gmail.com
See you in the next post.

Advertisements

2 thoughts on “BDD4Django Tutorial

  1. Pingback: Tutorial BDD4Django (Part 2) | CodEvening

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s