Tutorial de BDD4Django Parte 2 (Português)

Olá novamente.
Como prometido aqui está a segunda parte do tutorial de BDD4Django.

Se você não leu a parte 1 deste tutorial, aqui está para melhor acompanhar: Parte 1

Nesta parte criarei um exemplo mais complexo para mostrar algumas funcionalidades do BDD4Django ainda mostrarei as capacidades de testes rodando no browser.
O exemplo que irei usar é baseado na aplicação de enquetes(polls) presente neste tutorial: https://docs.djangoproject.com/en/dev/intro/tutorial01

Primeiro vamos definir que funcionalidades nossa aplicação precisará.

  • O usuário deve poder cadastrar novas enquetes
  • O usuário deve poder associar escolhas à enquetes
  • Um usuário pode votar em uma enquete
  • Cada usuário pode votar apenas uma vez em cada enquete
  • Um usuário pode ver os resultados da enquete (total de votos da enquete e percentual de cada opção)
  • Após votar o usuário é redirecionado para a página de resultados.

Os primeiros 2 itens podem ser feitos usando o admin do Django, e isso torna tudo bem mais fácil =)

Para votar numa enquete nós precisaremos de uma view e um form.
Para garantir que cada usuário vote apenas uma vez precisaremos armazenar o usuário e seu respectivo voto, além disso o usuário deve estar logado para votar.
Precisaremos de outra view e template para mostrar o resultado da enquete e, claro, um redirecionamento após o voto para exibir a página correta.
A aplicação demo que iremos criar nesse tutorial está disponível no pacote do BDD4Django e pode ser feito download dela via github ou via pip como falado no post anterior 😉

Primeiro crie um projeto Django e uma aplicação chamada polls.
Você precisará preparar o banco de dados e a adicionar a aplicação polls para o INSTALLED_APPS no settings do projeto.
Então crie uma aplicação para armazenar os templates, irei chama-la de ‘theme’, e adicione também ao INSTALLED_APPS.
Ah, e não se esqueça de incluir o caminho em STATICFILE_DIRS (se você está usando Django >=1.4)

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

Ainda você precisa descomentar as linhas relacionadas ao admin (em urls.py e settings.py) e adicionar a variável LOGIN_URL apontando para /admin nos settings do projeto, dessa forma poderemos usar a página de admin para efetuar login/logout.

LOGIN_URL = '/admin'

Primeiro de tudo, vamos testar o core das nossas views.
Como eu disse, precisaremos de uma view para computar os votos e outra para mostrar os resultados.
Após o usuário votar, ele será redirecionado para a página de resultados.
O usuário precisa estar logado para votar e a view deve receber um argumento (o id da enquete), dessa forma a view saberá qual enquete está sendo votada, o mesmo para a view que mostrará o resultado.

Uma das coisas mais legais a respeito de BDD é que ela torna muito mais intuitivo o processo de TDD, definir os testes antes de começar a escrever o código se torna muito mais fácil.
Vamos tentar converter a descrição das funcionalidades em um arquivo .feature.

Crie o seguinte arquivo polls.feature no diretório da sua app.

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, mas parece um pouco incompleto. O que my_choice significa? Nós ainda não temos enquete alguma definida.
Então vamos imaginar (e escrever) a seguinte enquete com suas respectivas opções.

Question: What’s the best Python IDE/Editor?
Choices:

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

Agora vamos modificar nosso arquivo .feature para prever essa enquete.

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/"

Primeiro eu carrego o valor da enquete com id 1 (A unica que criaremos) numa variável membro, então eu carrego o objeto da opção relativo a minha escolha em outra variável.
Depois chamo a view responsável por receber os votos e redirecionar para a página de resultaods.

Ok, mas eu preciso checar os valores no template que sou redirecionado, então adiciono as seguintes linhas:

   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

O número total de votos deve ser 1 (claro, é apenas um voto), e seu percentual é 100%.
Precisamos testar com diferentes usuários votando (lembre-se que é permitido apenas um voto por usuário por enquete) então vamos adicionar mais valores usando a tabela de valores do Morelia, teremos isso:

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%           |

Agora estamos testando diversos usuários diferentes votando (incluindo alguns repetidos) e o total de votos é incremento a cada voto (exceto os repetidos), e o percentual é atualizado.

Agora precisamos criar o arquivo tests.py para fazer o parse do arquivo .feature.

# *-* 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')

Você só precisa herdar da classe BDDCoreTestCase, e implementar o método test_evaluate_file, chamando o método parse_feature_file e o extra_setup que instanciará o self.client com um fake client.
O arquivo foi nomeado polls.feature, então o método parse_feature_file só precisa do parâmetro ‘polls’.

Rode e você receberá o seguinte:

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'

Claro, precisamos criar os modelos.
Precisaremos de um modelo para a enquete (Poll), um modelo para armazenar as opções de escolhas (Choice), e um modelo para associar os votos com o usuário (um usuário pode votar apenas uma vez na enquete).
Aqui está como fiz:

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")

Como pode ver implementei alguns métodos para ajudar (total_votes, percent_for_choice)
Rode o syncdb e execute os testes novamente.

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

Agora parece que não temos os objetos no banco de dados, precisamos cria-lo.
Como podemos criar objetos no banco de dados para testes? Django vem com um sistema padrão de fixtures, mas ele não é lá muito flexível, então optei por usar um app de factory para gerar a massa de dados para testes.
Escolhi o app model_mommy, você pode ler mais sobre ele no github do projeto: https://github.com/vandersonmota/model_mommy
Iremos usar apenas o método make_one do model_mommy, ele permite criar obetos facilmente, você pode especificar os dados ou deixar que ele gere dados aleatoriamente para os testes.

Para popular o banco de dados é necessário sobreescrever o método prepare_database, então aqui está a classe modificada:

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(',')]

Então mãos à obra pra criar a enquete que falamos acima.

Rode os testes manage.py test polls e você receberá o erro:

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

Claro, temos que implementar as views para votar e ver os resultados, então aqui está.

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)
    )

Aqui criamos 2 views: A view “vote”, que recebe o id da enquete (Poll) como argumento, salva o voto e redireciona para a view total_votes, esta por sua vez renderiza para o template o total de votos e o percentual de cada escolha.

Também precisamos de um form para votar, aqui está sua implementação:

# *-* 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)

Outra coisa que precisamos são os templates.

Aqui estão os templates mais simples do mundo.

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>

E precisamos de uma página 500.html e uma 404.html

500.html

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

404.html

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

Se você rodar os testes novamente você receberá outro erro:

       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/'

Como você pode ver nós fomos redirecionados para a página de admin, isso está acontecendo porque o login falha. Claro que falha, ainda não criamos nossos usuários, como logariamos?

Para nos ajudar eu criei um arquivo json de dados de usuários a serem carregados como uma fixture simples do Django.
Eu o chamei de polls_user.json, coloque ele num diretório fixtures sob a raiz do diretório da aplicação.

[
    {
        "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"
        }
    }
]

Na sua classe de testes adicione a linha:

fixtures = ['poll_users.json',]

Agora rode os testes e você deve ver algo parecido com isso:

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

Os testes executaram com sucesso, que tal agora testar a aplicação em um cenário interativo com o browser rodando?
Para isso crie outro arquivo .feature, chame-o de “polls_browser.feature” e vamos definir alguns cenários de testes.

O cenário mais óbvio é reescrever o cenário anterior, porém pensando na interação com o 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%           |

A ideia é a mesma, mas agora eu interajo com o browser.
A linha “I fill in field…” me diz para preencher o campo “choice” do form com o valor “eval:self.choice.id”.
Sempre que um valor inicia com “eval:” significa que é uma expressão de Python.
A linha “I submit the form” é auto-explicativa.
As linhas restantes são similares ao cenário anterior.

Outros cenários que precisamos testar:

    • Quando um usuário não logado tenta acessar a página de votação ele deve ser direcionado para a página de login do admin.
    • Quando um usuário tenta visitar uma página de enquete que não existe ele deve ver uma página de erro 404.
    • O usuário deve poder adicionar novas enquetes e escolhas para enquetes via página admin.

No mesmo arquivo .feature adicione os cenários:

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  |

Aqui estamos testando as features que listei acima, a maioria dos passos descritos aqui você já deve estar familiarizado, vamos falar sobre 2 em particular.

And I fill in fields “poll,choice_text” with values “Choose your favorite videogame character,
Esse passo preenche todos os campos listados entre aspas e separados por vírgula após “fields” com os valores também separados por vírgula e entre aspas logo após “values”.

And I see an object “polls.models.Choice” with values “{‘choice_text’:”}”
Esse passo verifica a existência de um objeto no banco de dados, os valores são referenciados na forma de um dicionário Python.

Após isso precisamos criar uma classe que faz o parse desse arquivo .feature, então em tests.py adicione as linhas:

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(',')]

Como pode ver estamos referenciando o caminho do arquivo (file_path) na chamada do método parse_feature_file, apenas passe ele como argumento em file_path (o caminho é relativo à raiz do projeto).
E estamos criando os dados no banco novamente, você pode mover esse código para outro método ou função pelo amor de DRY. Para manter esse tutorial o mais simples possível deixarei o código presente em ambas as classes.

Para executar apenas os testes presentes nessa classe digite:
./manage.py test polls.PollsBrowserTest

Você deve ver o resultado na tela:

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

Você está recebendo esse erro porque estamos testando a página de admin para adicionar as escolhas, mas não registramos nossos modelos para o admin.
Então crie um arquivo admin.py e adicione as linhas:

from django.contrib import admin
from polls.models import Poll, Choice

admin.site.register(Poll)
admin.site.register(Choice)

Agora rode os testes novamente e tudo ficará bem.
Os testes serão executados no Firefox, mas se quiser testar no Chrome você só precisa colocar a variável BDD_BROWSER no seu settings.py com o valor “chrome” conforme abaixo:

BDD_BROWSER = 'chrome'

E que tal testar tudo agora? Apenas execute: ./manage.py test polls e você terá o resultado a seguir:

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

É isso, espero que esse tutorial tenha te ajudado a usar BDD4DJango em seus projetos. Qualquer dúvida, sugestão ou aviso de bug você pode me contactar em daniel[dot]franca[at]gmail[dot]com.
A documentação completa para BDD4Django está disponível no meu github: https://github.com/danielfranca/BDD4Django

Te vejo nos próximos 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