BDD4Django Tutorial (Part 2)

Hello again.
As promised here comes the second part of BDD4Django tutorial.

If you didn’t read the part 1 of this tutorial, here’s the link to keep in track: Part 1

In this part I’ll create a more complex example to show the features of BDD4Django and yet show the browser tests BDD4Django capabilities.
The example I’m going to use is based on the polls app of this Django tutorial: https://docs.djangoproject.com/en/dev/intro/tutorial01
First let’s define what features our application needs.

  • The user must be able to register new polls
  • The user must be able to associate choices to polls
  • An user can vote in a poll
  • Each user can vote only one time in a poll
  • An user can see the results (total votes for a poll and the percent of each choice)
  • After vote the user is redirected to the total votes page.

The first 2 items can use the Admin pages of Django, and it’s going to be easy =)
To vote in a poll we’ll need a view and a form.
To guarantee that each user votes only once we’ll need to store the user and its vote, and the user must be logged to vote.
We’ll need another view and template to show the poll results and just a redirection after vote to show the correct page.
The demo application that we’ll gonna create in this tutorial is available in the BDD4Django package and can be downloaded via github or pip as talked in the preview post 😉
First create a Django project and a application called polls.
You’ll need to setup the database and add the polls application to the INSTALLED_APPS in settings.py
Then create an app to store the templates, I’m gonna call ‘theme’, and add it to the INSTALLED_APPS too, and don’t forget to include the path to STATICFILE_DIRS (if you are using Django >=1.4)

STATICFILES_DIRS = (
      os.path.join(PROJECT_ROOT,'theme'),
      )

Yet you’ve to uncomment the lines related to admin (in urls.py and settings.py) and add the LOGIN_URL pointing to /admin in the settings.py because we’ll use the admin page to login/logout.

LOGIN_URL = '/admin'

First of all, let’s test the core of ours views.
Like I said, we’ll need a view to compute the votes, and another view to show the results.
After the user votes, he’ll be redirected to the results page.
The user must be logged to vote and the view should receive an argument (the id of the poll), this way the view can know what poll is going to be voted, the same about the view that shows the total votes.
One of the nice things about BDD is that it turns a lot more intuitive the TDD process, define the test before write code becomes easy. Let’s try to convert the feature into a .feature file.

Create the following polls.feature file in your app directory:

Feature: Polls
Scenario: User vote
   When I call view "vote,args=(1,)" with post data "{'choice':my_choice}" as user "user01" with password "test"
  Then I'm redirected to url "/total_votes/1/"

Ok, but it looks a little bit incomplete. What my_choice means? We’ve no poll defined yet.
So let’s imagine the following poll and its choices:
Question: What’s the best Python IDE/Editor?
Choices:

  • WingIDE
  • PyCharm
  • Vi
  • Gedit
  • Eclipse
  • Aptana

So let’s modify our .feature file predicting this poll.

Feature: Polls
Scenario: User vote
  When I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
   And I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
   And I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
  Then I'm redirected to url "/total_votes/1/"

Fisrt I load the value of the poll with id 1 (the one we’ll create) in a member variable, so I load the choice object related to my choice in another member variable.
Then I call the view responsible to receive the votes and redirect to the results page.

Ok, but I need to check the values in the template I’m redirected to, so add the following lines:

   And I see the text "<td>Total Votes</td><td>1</td>" in template
   And I see the text "<td>Vi</td><td>100%</td>" in template

The total number of votes should be 1 (of course, it’s only one vote), and its percent is 100%.
We need to test different users voting (because it’s allowed only one vote per user per poll) so Let’s add some more values tests using the Morelia table feature, and we get:

Feature: Polls
Scenario: User vote
  When I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
   And I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='<choice_text>')" in "self.choice"
   And I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "<username>" with password "test"
  Then I'm redirected to url "/total_votes/1/"
   And I see the text "<td>Total Votes</td><td><total_votes></td>" in template
   And I see the text "<td><choice_text></td><td><votes_percent></td>" in template

  | username | choice_text | total_votes | votes_percent |
  | user01   | Vi          | 1           | 100%          |
  | user02   | Gedit       | 2           | 50%           |
  | user03   | PyCharm     | 3           | 33%           |
  | user04   | PyCharm     | 4           | 50%           |
  | user01   | PyCharm     | 4           | 50%           |
  | user05   | Eclipse     | 5           | 20%           |
  | user01   | Eclipse     | 5           | 20%           |
  | user06   | Vi          | 6           | 33%           |
  | user07   | WingIDE     | 7           | 14%           |

Now we are testing a lot of different users voting (including repeated ones) and the total votes are incrementing each vote (except the repeated ones), and the percent is updated.

Now you need to create the tests.py file that parses the .feature file.

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

from bdd4django.helpers import BDDCoreTestCase

class PollsCoreTest(BDDCoreTestCase):

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

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

You only need to extend the BDDCoreTestCase class, and implement the test_evaluate_file method, calling the parse_feature_file method and the extra_setup method instantiating the self.client with the fake client.
All file was named polls.feature, so the parse_feature_file only needs the ‘polls’ argument.

Run it and you’ll get something like the output.

e 3, in \nScenario: User vote
       When: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"\n
'module' object has no attribute 'Poll'

Sure, we’ve to create the models.
We’ll need a model for Poll, a model to store the choice options, and a model to associate the votes (an user can vote only once in a poll).
Here’s how I did it:

from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User

class Poll(models.Model):
    question = models.CharField(_('Question'),max_length=200)
    pub_date = models.DateTimeField(_('Date published'), auto_now_add=True, auto_now=True)

    def __unicode__(self):
        return self.question

    def total_votes(self):
        return sum([choice.votes_for_choice() for choice in Choice.objects.filter(poll=self)])

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice_text = models.CharField(_('Choice text'), max_length=200)

    def __unicode__(self):
        return self.choice_text

    def votes_for_choice(self):
        return PollChoice.objects.filter(choice=self).count()

    def percent_for_choice(self):
        return (PollChoice.objects.filter(choice=self).count()*100)/(self.poll.total_votes() or 1)

class PollChoice(models.Model):
    poll   = models.ForeignKey(Poll)
    user   = models.ForeignKey(User)
    choice = models.ForeignKey(Choice)

    class Meta:
        unique_together = ("user", "poll")

As you can see we implemented some methods to help us (total_votes,percent_for_choice)
Run syncdb and run the tests again.

Scenario: User vote
       When: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"\n
Poll matching query does not exist.

Now it seems that we don’t have the objects in the database, we’ve to create it.
How can we create objects in database for tests? Django comes with a default fixture system, but it’s not that flexible so I’m going to use a factory app to generate the data for tests.

I’ve chosen the model_mommy app, you can read more about it in the github page of the project: https://github.com/vandersonmota/model_mommy
We’ll use only the make_one method of model_mommy, it allows to create objects easily, you can specify the data or let model_mommy generate random data.

We remember from the last post that to populate the database we must to implement the prepare_database method, so here it goes the class modified:

from polls.models import Poll, Choice

class PollsCoreTest(BDDCoreTestCase):

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

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

    def prepare_database(self):
        poll  = mommy.make_one(Poll, question=u"What's the best Python IDE/Editor")
        choices = "WingIDE,PyCharm,Vi,Gedit,Eclipse,Aptana"
        [mommy.make_one(Choice, poll=poll, choice_text=choice_text) for choice_text in choices.split(',')]

So I just created the poll we talk about.

Run the tests and I get another error

Could not import polls.views.total_votes. View does not exist in module polls.views.

We have to implement the views to vote and see the results. So let’s go.

from django.shortcuts import render_to_response, redirect, get_object_or_404
from django.template.context import RequestContext
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required

from polls.models import Poll, Choice, PollChoice
from polls.forms import PollChoiceForm

@login_required
def vote(request, poll_id):

    #Check the existence of the poll
    poll = get_object_or_404(Poll, id=poll_id)

    #If the user already voted redirects to the total votes page
    if PollChoice.objects.filter(user=request.user, poll=poll).exists():
        return redirect(reverse('total_votes',kwargs={'poll_id': poll.id,}))

    form = PollChoiceForm(poll, request.POST or None)

    if form.is_valid():
        poll_choice = form.save(commit=False)
        poll_choice.user = request.user
        poll_choice.poll = poll
        poll_choice.save()

        return redirect(reverse('total_votes',kwargs={'poll_id': poll.id,}))

    return render_to_response(
        'polls/vote.html',
        {
            'poll': poll,
            'form': form,
        },
        context_instance=RequestContext(request)
    )


def total_votes(request, poll_id):

    poll = get_object_or_404(Poll, id=poll_id)
    #Get the percent of votes for the poll
    percent_votes = [(choice.choice_text, choice.percent_for_choice()) for choice in Choice.objects.filter(poll=poll)]

    return render_to_response(
        'polls/total_votes.html',
        {
            'poll': poll,
            'percent_votes': percent_votes,
            'total_votes': poll.total_votes(),
        },
        context_instance=RequestContext(request)
    )

Here we just created 2 views: The “vote” view, that receives the poll id as argument, save the vote and redirect to the total votes page and the “total_votes” that render to a template the total of votes and the percent of each choice.

We need a form to vote too, so here it’s

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

from django import forms
from django.utils.translation import ugettext_lazy as _

from polls.models import PollChoice,Choice

class PollChoiceForm(forms.ModelForm):

    class Meta:
        model = PollChoice
        fields = ('choice',)

    def __init__(self, poll, *args, **kwargs):
        super(PollChoiceForm, self).__init__(*args, **kwargs)
        self.fields['choice'] = forms.ModelChoiceField(queryset=Choice.objects.filter(poll=poll), label=_('Choice'), widget=forms.widgets.RadioSelect)

We need some templates to display the views results too:

Here’s the most simple templates in the world

vote.html

<html>
    <body>
        <h3>{{ poll.question }}</h3>
        <form method='POST'>
            {% csrf_token %}
            <p>{{ question }}</p>
            {{ form.as_p }}
            <input type="submit" value="submit"/>
        </form>

    </body>
</html>

total_votes.html

<html>
    <body>
        <h3>{{ poll.question }}</h3>

        <table>
            {% for choice_percent in percent_votes %}
                <tr>
                <td>{{ choice_percent.0 }}</td><td>{{ choice_percent.1 }}%</td>
                </tr>
            {% endfor %}
            <tr>
                <td>Total Votes</td><td>{{ total_votes }}</td>
            </tr>

        </table>
    </body>
</html>

And we need a 500.html and a 404.html

500.html

<html>
   <body>
    Internal server error
   </body>
</html>

404.html

<html>
   <body>
    Page not found
   </body>
</html>

If you run the tests again you’ll get another error:

       Then: I'm redirected to url "/total_votes/1/"\n
Response redirected to 'http://testserver/admin/?next=%2Fvote%2F1%2F', expected 'http://testserver/total_votes/1/'

As you can see we’re being redirected to the admin page, that’s happening because the login is failing.
Of course, we didn’t create any user yet. How we can login?

To help us I just created a json data file to load as a simple Django fixture.
I named it poll_users.json, put it in a fixture directory under the app root.

[
    {
        "pk": 4, 
        "model": "auth.user", 
        "fields": {
            "username": "user01", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:06.598Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$W5qJZWLRWR7A$it0p7zNEi0i6ASGhXLL8yqaf//sd9NQWfKx9Nnjuq9k=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:06.598Z"
        }
    }, 
    {
        "pk": 5, 
        "model": "auth.user", 
        "fields": {
            "username": "user02", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:12.822Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$EItYDCWBlJCZ$PJ0E4TGx7lNUD5a6unaypQgCAY2xlTidg7PxVtV+E44=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:12.822Z"
        }
    }, 
    {
        "pk": 6, 
        "model": "auth.user", 
        "fields": {
            "username": "user03", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:16.831Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$Hd1stLkPCXfD$qBi6UPwFJTRdSiRhO//mrmHOMsWT9HwLDKaRDZEkKOs=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:16.831Z"
        }
    }, 
    {
        "pk": 7, 
        "model": "auth.user", 
        "fields": {
            "username": "user04", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:24.142Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$UMRDOStGuHad$iyH6kxv1mDeSBcBIOsRE5lXEGXHPrTgmxf54EkJnpeo=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:24.142Z"
        }
    }, 
    {
        "pk": 8, 
        "model": "auth.user", 
        "fields": {
            "username": "user05", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:27.686Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$YUDCFJHa2uvb$Iio/sTFdMNaFHKUqH8y2Y1wgBKfPjpMsfhObLNNrK4k=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:27.686Z"
        }
    }, 
    {
        "pk": 9, 
        "model": "auth.user", 
        "fields": {
            "username": "user06", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:31.390Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$zMBnSGwvtf3V$kCjRdeJKm2da/TfCC1L9o8hgcCFZMWNkaq/JnNEstEA=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:31.390Z"
        }
    }, 
    {
        "pk": 10, 
        "model": "auth.user", 
        "fields": {
            "username": "user07", 
            "first_name": "", 
            "last_name": "", 
            "is_active": true, 
            "is_superuser": true,
            "is_staff": true, 
            "last_login": "2012-10-29T03:29:34.926Z", 
            "groups": [], 
            "user_permissions": [], 
            "password": "pbkdf2_sha256$10000$u5FLPfCKDRMV$t0hsxMFFIzUHbBjHayA9dAh4aR9A44ai9TtoaSdq5L4=", 
            "email": "", 
            "date_joined": "2012-10-29T03:29:34.926Z"
        }
    }
]

In your test class add the line:

fixtures = ['poll_users.json',]

Now just run the tests and you should see something like this:

Creating test database for alias 'default'...

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>1</td>" in template
Step: I see the text "<td>Vi</td><td>100%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Gedit')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user02" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>2</td>" in template
Step: I see the text "<td>Gedit</td><td>50%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user03" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>3</td>" in template
Step: I see the text "<td>PyCharm</td><td>33%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user04" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>4</td>" in template
Step: I see the text "<td>PyCharm</td><td>50%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>4</td>" in template
Step: I see the text "<td>PyCharm</td><td>50%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Eclipse')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user05" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>5</td>" in template
Step: I see the text "<td>Eclipse</td><td>20%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Eclipse')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>5</td>" in template
Step: I see the text "<td>Eclipse</td><td>20%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user06" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>6</td>" in template
Step: I see the text "<td>Vi</td><td>33%</td>" in template

**** Scenario: User vote
Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='WingIDE')" in "self.choice"
Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user07" with password "test"
Step: I'm redirected to url "/total_votes/1/"
Step: I see the text "<td>Total Votes</td><td>7</td>" in template
Step: I see the text "<td>WingIDE</td><td>14%</td>" in template
.
----------------------------------------------------------------------
Ran 1 test in 2.604s

OK

The tests ran fine, how about to test the application in a interactive browser scenario?
Create another .feature file, name it “polls_browser.feture” and let’s define some scenarios.

The obvious one is to rewrite the last scenario, but thinking in the browser.

Feature: Polls
Scenario: Vote in the browser
  When I visit url "/admin"
   And I login as "<username>" with password "test"
   And I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
   And I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='<choice_text>')" in "self.choice"
   And I visit url "/vote/1"
   And I fill in field "choice" with value "eval:self.choice.id"
   And I submit the form
  Then I'm redirected to url "/total_votes/1/"
   And I see the text "Total Votes <total_votes>"
   And I see the text "<choice_text> <votes_percent>"

  | username | choice_text | total_votes | votes_percent |
  | user01   | Vi          | 1           | 100%          |
  | user02   | Gedit       | 2           | 50%           |
  | user03   | PyCharm     | 3           | 33%           |
  | user04   | PyCharm     | 4           | 50%           |
  | user05   | Eclipse     | 5           | 20%           |
  | user06   | Vi          | 6           | 33%           |
  | user07   | WingIDE     | 7           | 14%           |

The idea is the same, but now I’ve to interact with the browser.
The line “I fill in field…” tells to fill the form field “choice” with value “eval:self.choice.id”.
Everytime a value starts with “eval” means that it’s a Python evaluation.
The line “I submit the form” is self-explanatory.
The other lines is similar to the old scenario.

Another scenarios we need to test:

  • When a not logged user tries to access the vote page he needs to be redirected to the admin page (for login).
  • When the user visits a non-existent poll, he must see a 404 error page.
  • The user must be able to add new polls and choices via admin page.
  • In the same .feature file add the scenarios:

    Scenario: Redirect to login
      When I visit url "/vote/1"
      Then I'm redirected to url "/admin/?next=/vote/1/"
    
    Scenario: Page not found
      When I visit url "/admin"
       And I login as "user01" with password "test"
       And I visit url "/vote/2"
      Then I see the text "Page not found"
    
    Scenario: Add new poll
      When I visit url "/admin"
       And I login as "user01" with password "test"
       And I visit url "/admin/polls/poll/add"
       And I fill in field "question" with value "Choose your favorite videogame character"
       And I submit the form
       Then I see the text "was added successfully."
    
    Scenario: Add new choices
      When I visit url "/admin"
       And I login as "user01" with password "test"
       And I visit url "/admin/polls/choice/add"
       And I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,<choice_text>"
       And I submit the form
       Then I see the text "was added successfully."
        And I see an object "polls.models.Choice" with values "{'choice_text':'<choice_text>'}"
    
        | choice_text |
        | Mario       |
        | Solid Snake |
        | Link        |
        | Pac-man     |
        | Cloud (FF7  |
    

    Here we are testing the features I just listed above, most of the steps described above you are already familiar, let me talk about 2 of them in particular.

    And I fill in fields “poll,choice_text” with values “Choose your favorite videogame character,
    This one fill all the comma separated fields listed in the quote after ‘fields’ with values comma separated too and listed in a quote just after ‘values’.

    And I see an object “polls.models.Choice” with values “{‘choice_text’:”}”
    This step checks the existence of the object in the database, the values are referenced like a Python dictionary.

    After that we need to create the class that parse this .feature file, so in the tests.py file add the lines:

    from bdd4django.helpers import BDDTestCase
    
    class PollsBrowserTest(BDDTestCase):
    
        fixtures = ['poll_users.json',]
    
        def test_evaluate_file(self):
            self.parse_feature_file(file_path='polls/polls_browser.feature')
    
        def prepare_database(self):
            r'a poll "([^"]+)" with choices "([^"]+)"'
            poll  = mommy.make_one(Poll, question=u"What's the best Python IDE/Editor")
            choices = "WingIDE,PyCharm,Vi,Gedit,Eclipse,Aptana"
            [mommy.make_one(Choice, poll=poll, choice_text=choice_text) for choice_text in choices.split(',')]
    

    As you can see we are now referencing the file path in the parse_feature_file call, just set the argument file_path (the path is project root relative).
    And we are creating the data in the database again, you can move this code to another method or function for DRY’s sake. To keep this tutorial simpler I’ll leave the code in both classes.

    To run only this test class type:
    ./manage.py test polls.PollsBrowserTest

    You should receive the output:

       raise HttpResponseError(self.code, self.reason)
    HttpResponseError: 404 - Not Found
    

    You are receiving this error because we are testing the admin page to add polls and choices, but it didn’t register our models for admin.
    So just create an admin.py page and add the lines:

    from django.contrib import admin
    from polls.models import Poll, Choice
    
    admin.site.register(Poll)
    admin.site.register(Choice)
    

    Now run the tests again and everything should be fine.
    The tests will run in Firefox, but if you want to test in Chrome you only have to put the variable BDD_BROWSER in your settings.py with value “chrome” as bellow:

    BDD_BROWSER = 'chrome'
    

    And how about to test everything? just execute: ./manage.py test polls and you’ll get a similar output:

    Creating test database for alias 'default'...
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>1</td>" in template
    Step: I see the text "<td>Vi</td><td>100%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Gedit')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user02" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>2</td>" in template
    Step: I see the text "<td>Gedit</td><td>50%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user03" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>3</td>" in template
    Step: I see the text "<td>PyCharm</td><td>33%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user04" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>4</td>" in template
    Step: I see the text "<td>PyCharm</td><td>50%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>4</td>" in template
    Step: I see the text "<td>PyCharm</td><td>50%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Eclipse')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user05" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>5</td>" in template
    Step: I see the text "<td>Eclipse</td><td>20%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Eclipse')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user01" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>5</td>" in template
    Step: I see the text "<td>Eclipse</td><td>20%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user06" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>6</td>" in template
    Step: I see the text "<td>Vi</td><td>33%</td>" in template
    
    **** Scenario: User vote
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='WingIDE')" in "self.choice"
    Step: I call view "vote,args=(1,)" with post data "{'choice':self.choice.id}" as user "user07" with password "test"
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "<td>Total Votes</td><td>7</td>" in template
    Step: I see the text "<td>WingIDE</td><td>14%</td>" in template
    .
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 1"
    Step: I see the text "Vi 100%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user02" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Gedit')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 2"
    Step: I see the text "Gedit 50%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user03" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 3"
    Step: I see the text "PyCharm 33%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user04" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='PyCharm')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 4"
    Step: I see the text "PyCharm 50%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user05" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Eclipse')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 5"
    Step: I see the text "Eclipse 20%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user06" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='Vi')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 6"
    Step: I see the text "Vi 33%"
    
    **** Scenario: Vote in the browser
    Step: I visit url "/admin"
    Step: I login as "user07" with password "test"
    Step: I load value "polls.models.Poll.objects.get(id=1)" in "self.poll"
    Step: I load value "polls.models.Choice.objects.get(poll=self.poll,choice_text='WingIDE')" in "self.choice"
    Step: I visit url "/vote/1"
    Step: I fill in field "choice" with value "eval:self.choice.id"
    Step: I submit the form
    Step: I'm redirected to url "/total_votes/1/"
    Step: I see the text "Total Votes 7"
    Step: I see the text "WingIDE 14%"
    
    **** Scenario: Redirect to login
    Step: I visit url "/vote/1"
    Step: I'm redirected to url "/admin/?next=/vote/1/"
    
    **** Scenario: Page not found
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/vote/2"
    Step: I see the text "Page not found"
    
    **** Scenario: Add new poll
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/poll/add"
    Step: I fill in field "question" with value "Choose your favorite videogame character"
    Step: I submit the form
    Step: I see the text "was added successfully."
    
    **** Scenario: Add new choices
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/choice/add"
    Step: I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,Mario"
    Step: I submit the form
    Step: I see the text "was added successfully."
    Step: I see an object "polls.models.Choice" with values "{'choice_text':'Mario'}"
    
    **** Scenario: Add new choices
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/choice/add"
    Step: I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,Solid Snake"
    Step: I submit the form
    Step: I see the text "was added successfully."
    Step: I see an object "polls.models.Choice" with values "{'choice_text':'Solid Snake'}"
    
    **** Scenario: Add new choices
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/choice/add"
    Step: I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,Link"
    Step: I submit the form
    Step: I see the text "was added successfully."
    Step: I see an object "polls.models.Choice" with values "{'choice_text':'Link'}"
    
    **** Scenario: Add new choices
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/choice/add"
    Step: I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,Pac-man"
    Step: I submit the form
    Step: I see the text "was added successfully."
    Step: I see an object "polls.models.Choice" with values "{'choice_text':'Pac-man'}"
    
    **** Scenario: Add new choices
    Step: I visit url "/admin"
    Step: I login as "user01" with password "test"
    Step: I visit url "/admin/polls/choice/add"
    Step: I fill in fields "poll,choice_text" with values "Choose your favorite videogame character,Cloud (FF7"
    Step: I submit the form
    Step: I see the text "was added successfully."
    Step: I see an object "polls.models.Choice" with values "{'choice_text':'Cloud (FF7'}"
    .
    ----------------------------------------------------------------------
    Ran 2 tests in 81.757s
    
    OK
    

    That’s it, I hope this tutorial can help you to use BDD4Django in your own projects. Any doubt, suggestion or bug you can contact me in daniel[dot]franca[at]gmail[dot]com.
    The full documentation for BDD4Django is available in my github: https://github.com/danielfranca/BDD4Django

    See you in the next posts.

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