BDD em Django: Desenvolvimento web mais divertido com qualidade usando Freshen

Freshen é um framework Python para construção de testes de aceitação, baseado no Cucumber e tem o mesmo objetivo do Cucumber: fazer o desenvolvimento de softwares com BDD mais divertido. Podemos aplicar os conceitos do BDD escrevendo testes de aceitação em alto nível e graças à integração com o Nose, podemos ainda usar testes unitários para testar unidades de código.

Este post tem como objetivo fazer uma apresentação básica do uso do Django com o Freshen. Antes de começar, vamos preparar as ferramentas necessárias :) Como o Freshen é um plugin do Nose, para utilizar o Freshen, precisamos do Nose. Já que vamos integrar o Django também no esquema, precisamos também adicionar um plugin para testar o Django, NoseDjango, e também o próprio Django. Por fim, para isolar os tests do banco de dados, um framework de Mock é uma boa pedida. Vamos utilizar o Ludibrio, que foi feito por brasileiros, é simples e bom o suficiente. Se você tem o PIP instalado, um simples comando instala tudo que nós precisamos:

1
$ sudo pip install django nose nosedjango freshen ludibrio

Agora que temos todas as ferramentas necessárias, podemos começar a trabalhar no nosso “sisteminha”. Nosso exemplo vai ser um delivery de medicamentos para a farmácia do tio-avô da ex-namorada do meu primo de segundo grau. Vamos implementar apenas uma funcionalidade, que será descrita em um arquivo de funcionalidade.

Primeiramente, vamos criar o projeto do Django para a farmácia:

1
$ django-admin.py startproject farmacia

Criado o projeto, vamos iniciar a aplicação delivery, que vai ser o que vamos testar:

1
$ django-admin.py startapp delivery

Não há muita coisa a ser configurada no projeto, vamos apenas configurar o banco de dados, definir o diretório dos templates (no projeto) e adicionar nossa aplicação ao INSTALLED_APPS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'dados.db',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

#Outras configurações aqui...

import os
ROOT = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_DIRS = (
    os.path.join(ROOT, 'templates'),
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'delivery',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
)

Agora que temos nosso projeto iniciado e configurado, com nossa aplicação iniciada e instalada, vamos começar a desenvolver, mas primeiro vamos escrever as funcionalidades do Freshen. As funcionalidades do Freshen são compostas por cenários que por sua vez são compostos por passos. As funcionalidades de um sistema são definidas dentro de arquivos de funcionalidades. Vamos criar a funcionalidade Listar remédios, que será a listagem de remédios para serem selecionados para compra e entrega.

Vamos criar dentro da aplicação delivery um diretório chamado features e dentro deste diretório criaremos o arquivo listar_remedios.feature, com o seguinte conteúdo:

1
2
3
4
5
6
7
8
9
Funcionalidade: Listar remédios
    Para poder comprar um remédio
    Como um usuário do sistema
    Eu gostaria de ver uma lista contendo os remédios para eu escolher

    Cenário: Listando todos os remédios
        Dado que existem 10 remédios cadastrados no banco de dados
        Quando eu vou para a página de listagem de remédios
        Então eu deveria ver a listagem com o nome dos 10 remédios

O “código” da funcionalidade é totalmente simples: a funcionalidade tem um nome, no nosso caso “Listar remédios”, um cabeçalho, que é uma descrição livre da funcionalidade (você realmente pode escrever o que quiser no cabeçalho). Para cada história, definimos cenários, utilizando a palavra chave “Cenário:”. O cenário é composto por um nome e alguns passos, definidos pelas palavras-chave Given, When, Then, And e But (em português: Dado, Quando, Então, E e Mas). Para entender mais a organização do BDD e a definição de funcionalidades, cenários e passos, dê uma olhada em algum material sobre BDD (vastidão de material apenas em inglês =P).

Após criar a funcionalidade e o cenário, temos que definir os passos do nosso cenário. O cenário “Listando todos os remédios” possui três passos: “Dado que existem 10 remédios cadastrados no banco de dados”, “Quando eu vou para a página de listagem de remédios” e “Então eu deveria ver a listagem com o nome dos 10 remédios”. Precisamos definir o que o Freshen fará em cada um desses passos para garantir o funcionamento da funcionalidade especificada.

Vamos rodar o Freshen, para ver a saída neste momento. Quem sabe nosso teste passa sem fazermos nada =D

1
2
3
4
5
6
7
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... UNDEFINED: "que existem 10 remédios cadastrados no banco de dados" # delivery/features/listar_remedios.feature:7
Tests that 1 + 1 always equals 2. ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.175s
OK (UNDEFINED=1)
Destroying test database 'default'...

Vamos entender o comando executado: executamos o comando nosetests, que é o comando do Nose. Como o Freshen é um plugin do Nose, precisamos habilitá-lo utilizando a opção –with-freshen, e habilitamos o Django com a opção –with-django. Uma vez que escrevemos nossa funcionalidade em Português, temos que avisar ao Freshen que ele deve lê-la em português, por isso utilizamos a opção –language=pt. Por fim, utilizamos a opção -v para habilitar o modo verbose.

Agora, vamos entender a saída do comando: veja na primeira linha, que o Freshen encontrou funcionalidade “Listar remédios”, e dentro desta funcionalidade encontrou o cenário “Listando todos os remédios” com o passo “que existem 10 remédios cadastrados no banco de dados” indefinido. Então, vamos definir tal passo e executar o teste novamente. Os passos são definidos em um módulo chamado steps.py presente no mesmo diretório do arquivo da funcionalidade (features, dentro da aplicação).

Para definir os passos, utilizamos decorators correspondentes a estes passos: para o passo Given/Dado, utilizamos o decorator @Given; para o passo When/Quando, utilizanmos o decorator @When; e para o passo Then/Então, utilizamos o decorator @Then. Vamos definir nosso primeiro passo, utilizando o decorator @Given, e ver o que acontece ao executar o Freshen novamente após tal definição:

1
2
3
4
5
6
7
@Given('que existem (\\d+) remédios cadastrados no banco de dados')
def gravar_remedios(quantidade_remedios):
    scc.remedios = []
    for i in xrange(int(quantidade_remedios)):
        remedio = Remedio()
        remedio.nome = 'Remédio %d' %(i + 1)
        scc.remedios.append(remedio)

Note que nós recebemos um parâmetro para nossa função a partir do texto que define o passo. O formato deste parâmetro é definido por uma expressão regular. Atenção: o parâmetro passado para a função sempre é uma string, se você precisar trabalhar este parâmetro como um inteiro, ele deverá ser convertido (como aconteceu no nosso caso).

Na função acima, nós criamos uma lista chamada remedios dentro de um objeto chamado scc. Trata-se do contexto de armazenamento de objetos do Freshen. O Freshen da suporte a três tipos de contexto: glc, o contexto global, que nunca é apagado; ftc, o contexto de funcionalidade, que é apagado no início da execução de cada funcionalidade; e o scc, que é o contexto de cenário, apagado no início da execução de cada cenário. Quando armazenamos nossa lista de remédios dentro do contexto de cenário, significa que esta lista estará disponível em todos os passos deste cenário.

Agora, vamos executar novamente o Freshen para ver o que acontece:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... ERROR
Tests that 1 + 1 always equals 2. ... ok
======================================================================
ERROR: Listar remédios: Listando todos os remédios
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/francisco/Projetos/blog/farmacia/delivery/features/steps.py", line 9, in gravar_remedios
    remedio = Remedio()
NameError: global name 'Remedio' is not defined
>> in "que existem 10 remédios cadastrados no banco de dados" # delivery/features/listar_remedios.feature:7
----------------------------------------------------------------------
Ran 2 tests in 0.162s
FAILED (errors=1)
Destroying test database 'default'...

E o feedback obtido foi: não encontrei esse classe Remedio. Isto ocorre por que nós não definimos nosso model Remedio. Então, vamos definí-lo, com apenas um nome, dentro do módulo models.py da aplicação delivery, e executar novamente o teste:

1
2
3
from django.db import models
class Remedio(models.Model):
    nome = models.CharField(max_length=255)

Agora, vamos novamente rodar nossos testes e ver o que acontece:

1
2
3
4
5
6
7
8
9
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... UNDEFINED: "eu vou para a página de listagem de remédios" # delivery/features/listar_remedios.feature:8
Tests that 1 + 1 always equals 2. ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.198s

OK (UNDEFINED=1)
Destroying test database 'default'...

Nossa definição de passo agora passou, porém recebemos o feedback de outro passo indefinido: “eu vou para a página de listagem de remédios”. Vamos então definir este passo, utilizando o decorator @When:

1
2
3
4
5
6
7
8
@When('eu vou para a página de listagem de remédios')
def visitar_pagina_listagem():
    with Mock() as delivery:
        from delivery.models import Remedio
        Remedio.objects.all() >> scc.remedios
    client = Client()
    scc.response = client.get('/delivery/remedios')
    delivery.validate()

Vamos entender o que foi feito aqui: o método começa fazendo um mock da aplicação do Django, tudo isso para garantir o mock interno do model. Assim, separamos nossa definição do banco de dados: quando o método Remedio.objects.all() for chamado, o objeto de mock, gerenciado pelo Ludibrio, retornará a nossa lista de remédios armazenadas no contexto do cenário. Pulando um pouco, na última linha da função validamos o nosso mock, ou seja, verificamos se o comportamento que esperávamos dele realmente aconteceu. Traduzindo: esta última linha garante que o método realmente foi invocado, mas onde?

Na linha superior, utilizamos o Client do Django para fazer uma requisição do tipo GET na URL ‘/delivery/models’ do nosso projeto. A resposta desta requisição é armazenada dentro do nosso contexto de cenário, em um objeto chamado response. Vamos novamente executar o Freshen e ver o que acontece:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... ERROR
Tests that 1 + 1 always equals 2. ... ok

======================================================================
ERROR: Listar remédios: Listando todos os remédios
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/francisco/Projetos/blog/farmacia/delivery/features/steps.py", line 22, in visitar_pagina_listagem
    scc.response = client.get('/delivery/remedios')
  File "/usr/local/lib/python2.6/dist-packages/django/test/client.py", line 290, in get
    response = self.request(**r)
  File "/usr/local/lib/python2.6/dist-packages/django/core/handlers/base.py", line 127, in get_response
    return callback(request, **param_dict)
  File "/usr/local/lib/python2.6/dist-packages/django/views/defaults.py", line 13, in page_not_found
    t = loader.get_template(template_name) # You need to create a 404.html template.
  File "/usr/local/lib/python2.6/dist-packages/django/template/loader.py", line 157, in get_template
    template, origin = find_template(template_name)
  File "/usr/local/lib/python2.6/dist-packages/django/template/loader.py", line 138, in find_template
    raise TemplateDoesNotExist(name)
TemplateDoesNotExist: 404.html

>> in "eu vou para a página de listagem de remédios" # delivery/features/listar_remedios.feature:8

----------------------------------------------------------------------
Ran 2 tests in 0.370s

FAILED (errors=1)
Destroying test database 'default'...

E a resposta do Freshen ao nosso teste foi: Não encontrei o template 404.html! Nada a ver com nosso teste, certo? Acontece que, na verdade, a URL /delivery/remedios não foi encontrada e o Django tenta renderizar o template 404.html nestes casos, mas não importa isso no momento. Nosso desejo aqui é fazer a definição de passo passar, então vamo simplesmente colocar um arquivo chamado 404.html dentro do nosso diretório de templates, chamar o Freshen novamente e ver o que acontece:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ mkdir templates
francisco@radukibook ~/Projetos/blog/farmacia (master) $ touch templates/404.html
francisco@radukibook ~/Projetos/blog/farmacia (master) $ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... FAIL
Tests that 1 + 1 always equals 2. ... ok

======================================================================
FAIL: Listar remédios: Listando todos os remédios
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/francisco/Projetos/blog/farmacia/delivery/features/steps.py", line 23, in visitar_pagina_listagem
    delivery.validate()
  File "/usr/local/lib/python2.6/dist-packages/ludibrio/mock.py", line 103, in validate
    self._call_waiting_msg())
MockExpectationError: Call waiting:
Expected:
Remedio.objects.all() >> scc.remedios
Got only:

----------------------------------------------------------------------
Ran 2 tests in 0.373s

FAILED (failures=1)
Destroying test database 'default'...

A validação do mock falhou. Simplesmente por que o mock não teve o comportamento esperado, que é a chamada ao método Remedio.objects.all(). Vamos fazer agora com que a requisição na URL /delivery/remedios seja mapeada para uma view que deverá fazer a chamada ao nosso método.

Primeiro, vamos definir a nossa view: dentro do módulo view da aplicação delivery, vamos criar a função a ser mapeada para a URL /delivery/remedios. Esta função se chamará lista_remedios:

1
2
3
def listar_remedios(request):
    Remedio.objects.all()
    return HttpResponse('')

Note que aqui novamente fizemos apenas o necessário para que a definição de passo passe. Tudo o que precisamos é da chamada a Remedio.objects.all(). Mas apenas isso não é suficiente, precisamos ainda mapear a URL /delivery/remedios para a nossa view recém-definida. Para isso, criaremos o model urls.py dentro da aplicação delivery com o seguinte conteúdo:

1
2
3
4
5
from django.conf.urls.defaults import *

urlpatterns = patterns('delivery.views',
    url(r'^remedios', 'listar_remedios', name = 'listar_remedios'),
)

Depois de definir tal arquivo, vamos incluir dentro do módulo urls.py principal do projeto, que deverá ficar desta forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^farmacia/', include('farmacia.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # (r'^admin/', include(admin.site.urls)),
    (r'^delivery/', include('delivery.urls')),
)

Com o mapeamento de URLs configurado e a view devidamente criada, vamos novamente executar o Freshen para obter o feedback:

1
2
3
4
5
6
7
8
9
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... UNDEFINED: "eu deveria ver a listagem com o nome dos 10 remédios" # delivery/features/listar_remedios.feature:9
Tests that 1 + 1 always equals 2. ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.390s

OK (UNDEFINED=1)
Destroying test database 'default'...

A parte boa é que nossa definição para o segundo passou funcionou! Agora o Freshen encontrou outro passo indefinido: “eu deveria ver a listagem com o nome dos 10 remédios”. Vamos agora implementar este passo, testando o conteúdo da resposta obtida na requisição à URL /delivery/remedios. Para definir este passo, utilizamos o decorator @Then:

1
2
3
4
5
@Then('eu deveria ver a listagem com o nome dos (\d+) remédios')
def verificar_conteudo_listagem(quantidade_remedios):
    for remedio in scc.remedios:
        conteudo_esperado = '<li>%s</li>' %(remedio.nome)
        assert_true(conteudo_esperado in scc.response.content)

Montamos o elemento HTML li com o conteúdo esperado e depois testamos se todos os nomes de remédios estão presentes na resposta da requisição, cada um em seu respectivo li. Depois fazemos um assert, para verificar se o conteúdo que definimos acima está presente no conteúdo da resposta da requisição. Ao chamar novamente o Freshen, obtemos a seguinte saída:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... FAIL
Tests that 1 + 1 always equals 2. ... ok

======================================================================
FAIL: Listar remédios: Listando todos os remédios
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/francisco/Projetos/blog/farmacia/delivery/features/steps.py", line 31, in verificar_conteudo_listagem
    assert_true(conteudo_esperado in scc.response.content)
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.362s

FAILED (failures=1)
Destroying test database 'default'...

E aí está: um AssertionError, o que significa que nossa definição falhou e o conteúdo esperado não veio na resposta da requisição. Para fazer com que esta definição funcione, vamos refatorar nossa view, para que ela renderize um template e dentro deste template teremos nossos elementos li com os nomes dos remédios. Eis o código da view, refatorado:

1
2
3
4
5
6
def listar_remedios(request):
    remedios = Remedio.objects.all()
    return render_to_response('listar_remedios.html', {
            'remedios' : remedios
        }, context_instance=RequestContext(request)
    )

Então devemos criar um template chamado listar_remedios.html. Como é um template da aplicação, coloque ele dentro do diretório templates da aplicação delivery. O conteúdo do template pode ser visto no Gist: http://gist.github.com/450936

Acredito que aqui tudo está pronto. Vamos perguntar ao Freshen?

1
2
3
4
5
6
7
8
9
$ nosetests --with-freshen --with-django --language=pt -v
Listar remédios: Listando todos os remédios ... ok
Tests that 1 + 1 always equals 2. ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.366s

OK
Destroying test database 'default'...

Nossa definição de passo passou, e não existe nenhum passo indefinido! Nosso trabalho está pronto, agora basta repetir o ciclo para implementar todas as funcionalidades do sistema, mas isso fica por conta de vocês.

O código deste projeto está disponível no Github: http://github.com/franciscosouza/farmacia.

  • Share/Bookmark
Esta entrada foi publicada em desenvolvimento de softwares e marcada com a tag , , , , , , . Adicione o link permanenteaos seus favoritos.

2 respostas a BDD em Django: Desenvolvimento web mais divertido com qualidade usando Freshen

  1. Pingback: Tweets that mention BDD em Django: Desenvolvimento web mais divertido com qualidade usando Freshen | Francisco Souza -- Topsy.com

  2. Parabéns pelo artigo Francisco,
    fico muito legal.
    Abraços

Deixe uma resposta

O seu endereço de email não será publicado Campos obrigatórios são marcados *

*

Você pode usar estas tags e atributos de HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>