Tutorial de BDD4Django (Português)

BDD(Behavior Driven Development) é um processo de desenvolvimento de software baseado em TDD(Test Driven Development). IMHO ele torna o processo de TDD muito mais intuitivo, permitindo um melhor fluxo de trabalho e softwares de maior qualidade.

Neste post irei falar sobre BDD4Django, um pacote para Django que desenvolvi para usar BDD em seu projeto sem dores de cabeça.

Este pacote unifica 4 elementos

  • Django – O famoso framework web para Python.
  • Morelia Viridis (com novas features e melhor integração com Django) – Uma fantástica ferramenta BDD para Python.
  • Splinter – Biblioteca que permite testes diretamente no browser.
  • Algumas classes de TestCase que suportam muitos “passos” já pré-definidos.

Primeiro, vamos ao inicio.

Morelia é um framework BDD baseado no estilo Cucumber para Ruby.
Você escreve um arquivo .feature que permite que se escreva em um estilo de inglês convencional (Given-When-Then) conforme abaixo:


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 é um framework para testes diretamente no browser, com ele você consegue lançar e interagir com o browser, como clicar em botões, preencher campos de formulário, checar a presença de elementos e de texto sendo mostrado no browser, etc.

BDD4Django é um fork do Morelia, então você não precisa instalar Morelia se tiver o BDD4Django instalado.
Para instalar Splinter você pode seguir as instruções aqui: http://splinter.cobrateam.info/docs/install.html (ok, tudo que você precisa é usar o bom amigo pip)

BDD4Django requer Django 1.3+, mas se for Django < 1.4 você precisa instalar django-live-server https://github.com/adamcharnock/django-live-server (porque os testes rodam em um live server)

Tudo pronto podemos começar a programar!
BDD4Django tem 2 classes principais que você pode herdar: BDDTestCase ou BDDCoreTestCase.
A primeira é para teste diretamente no browser, a segunda é para testes de funcionalidades sem interação com browser.

Neste post irei falar sobre BDDCoreTestCase, nos próximos posts abordarei BDDTestCase.

Primeiro, instale o BDD4Django, para isso simplesmente digite no seu terminal:
pip install bdd4django
Então escreva sua aplicação Django, para seguir este tutorial crie uma app calculator com o comando:

manage.py startapp calculator

Então adicione a app em INSTALLED_APPS.
Primeiras coisas primeiro, crie um arquivo .feature para escrever os cenários, vamos começar com um simples cenário de soma de 2 números na nossa calculadora:

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 que o nome do método vem após o nome do módulo (a app no caso), e o parameters usa uma sintaxe de dicionário Python.

Agora você precisa escrever o arquivo tests.py como no exemplo abaixo:


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

from bdd4django.helpers import BDDCoreTestCase

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

Basta criar uma classe que herde de BDDCoreTestCase, e que implemente o método test_evaluate_file chamando o método parse_feature_file.
O parse_feature_file recebe o nome da app como primeiro argumento (O arquivo .feature deve estar na raiz do diretório da app e ter o nome <app_name>.feature, se precisa passar o nome do arquivo com o caminho completo (relativo ao projeto) utilize o argumento file_path.
Criamos o arquivo calculator.feature no diretório raiz da app, então utilize apenas ‘calculator’ como argumento.

Rode os testes (python manage.py test calculator), e você terá a saída:

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'

Claro que recebemos esse erro, afinal não implementamos o método utils.sum, então vamos faze-lo.
Crie um arquivo chamado utils.py e escreva nele:


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

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

Agora execute os testes novamente e você terá o resultado:


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

Parabéns, você executou o primeiro teste com sucesso.
Testar apenas um conjunto de parâmetros é uma forma pobre de testar, e escrever um cenário para cada conjunto pode ser entediante, então você pode escrever apenas um cenário com diferentes conjuntos de variáveis, apenas mude o arquivo calculator.feature desta forma:


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

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

Para cada cenário executado o teste substituirá os valores <x>, <y> e <result> por seus respectivos valores na linha da tabela e você poderá ver isso na saída da execução de testes conforme abaixo:


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

Mas e se você quiser carregar os valores antes da chamada do método?
Simplesmente implemente o cenário da seguinte forma:


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"

O resultado deve ser algo parecido com isso:


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

Como você pode ver todos os testes estão sendo executados, mas e se você quiser executar somente o ultimo cenário?
Então tudo que você precisa é adicionar um parâmetro scenarios na chamada do método parse_feature_file  (tests.py)  dessa forma.


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

O parâmetro scenarios é uma tupla de nome de cenários, no exemplo iremos executar apenas o ultimo cenário.

O BDD4Django te alerta que outros cenários serão ignorados:


!!!! Ignoring scenario: Sum two numbers

Vamos dizer que eu queira salvar os valores e resultados no banco de dados, então neste caso criamos um model simples como esse e sincronizamos o banco de dados:

from django.db import models

class Sum(models.Model):

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

E para checar se o objeto foi salvo no banco de dados eu mudo o ultimo cenário no arquivo calculator.feature.

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

Rode os testes novamente e você terá:

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

Nós não mudamos o método sum, então vamos fazer agora:

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

Rode os testes novamente… lá vai:

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

Tudo funcionando!

O método sum está ok, que tal irmos para o teste de view?

Primeiro, implemente o arquivo 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)
    )

A view recebe 2 parameters via POST method (x e y) e renderiza para um template chamado “calculator.html” o resultado da soma desses parâmetros.
Agora nós temos que implementar o arquivo de template.

Aqui está uma simples implementação de “calculator.html”

{% load i18n %}


{% trans "Calculator" %}


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


Adicione essa linha no seu urls.py para direcionar a view corretamente.

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

Para testar views precisamos sobreescrever o método extra_setup de BDDCoreTestCase, e inicializar duas propriedades (self.client e self.response).
self.cliente é o cliente HTTP fake da classe TestCase, e self.response armazena a resposta de uma requisição HTTP.

Aqui está minha nova classe MyTestCase:

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

Finalmente aqui está o novo cenário testando a soma via a view:

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

Você pode alterar a chamada do método parse_feature_file para rodar apenas o ultimo cenário:

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

Rode os testes, a saída deve ser a seguinte:

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

Adicionalmente você pode testar se a variável de contexto possue o valor correto.

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

E ainda posso checar se o resultado correto é mostrado no template.

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

Meu cenário final fica assim:

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

E a saída ao executar o cenário é essa abaixo:

!!!! 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'...

Que tal rodar todos os testes juntos?
Remova o parâmetro scenarios da chamada do método parse_feature_file no arquivo tests.py e rode novamente.

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

Existem algumas variantes desses métodos presentes neste tutorial (como chamar view com parâmetros; com dados via GET, chamada com usuário logado e outras), você pode ver a documentação completa do BDD4Django (em inglês) no meu github: https://github.com/danielfranca/BDD4Django

Próximo post falarei sobre a classe BDDTestCase e como rodar testes de integração diretamente no browser.

Espero que esse tutorial te ajude a se envolver com BDD e entender BDD4Django, sinta-se livre para me contactar em qualquer duvida, sugestão ou para criticar em daniel.franca [at] gmail.com

Vejo vocês no próximo post.

Advertisements

One thought on “Tutorial de BDD4Django (Português)

  1. Pingback: Tutorial de BDD4Django Parte 2 (Português) | 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