Django: Guia de testes
Osvaldo Santana Neto
Tweet Sobre Esse Livro!
Por favor ajude Osvaldo Santana Neto a divulgar esse livro noTwitter! O tweet sugerido para esse livro é:
Acabei de comprar o ebook ”Django: Guia de testes” do @osantana: http://j.mp/djteste A hashtag sugerida para esse livro é#django-testes.
Descubra o que as outras pessoas estão falando sobre esse livro clicando nesse link para buscar a hashtag no Twitter:
Conteúdo
Créditos . . . 1 Introdução . . . . 2 Django Tests . . . 2 O Projeto . . . 4 Primeiro Teste . . . 5CONTEÚDO 1
Créditos
• Autor e Capa:Osvaldo Santana Neto • Foto da Capa:Fabrizio Sciami
AVISO!
Esse livro ainda está em processo de escrita, portanto, as informações aqui podem estar incorretas. Além disso a revisão de escrita (gramática, ortografia) será feita apenas na etapa final. É bem provável que você encontre erros graves de português antes disso.
Introdução
Quando comecei a trabalhar com Django encontrei algumas dificuldades na criação de testes automati-zados em meus projetos. Minhas dificuldades principais eram:
• Fazer com que os testes unitários não manipulassem o banco de dados; • Escrever testes que executassem rápido;
• Testar só o meu código e não o framework.
Com o tempo eu me desapeguei de algumas crenças que tinha no desenvolvimento de testes unitários (ex. testes unitários não devem interagir com o banco de dados) e aceitei o “Jeito Django” de testar.
Mas eu tive que aprender isso na marra porque não encontrava bons tutoriais oferecendo uma abordagem mais prática e com exemplos baseados em aplicações de verdade. Só encontrava exemplos muito básicos e completamente descolados do “mundo real”.
Vamos trabalhar com o Django 1.7 mas os exemplos devem funcionar sem maiores problemas com o Django 1.6. Você perceberá que também uso Python 3 nos exemplos.
Django Tests
A distribuição padrão do Django trás quase tudo o que é necessário para testarmos nossas aplicações e por isso vou focar nessas ferramentas.
Existe um bom conjunto de ferramentas e aplicações Django voltadas para o desenvolvimento de testes no siteDjango Packages.
Organizando os testes
Um projeto Django ideal tem as suas funcionalidades separadas em várias aplicações (applications). O modo padrão de organizar os testes de uma aplicação é criando módulos ou packages Python com nomes seguindo o formatotest*. Usaremos o formato:app/tests/test_*.py.
Dentro desses módulos você pode ter uma ou mais subclasses de umTestCasecom métodos denominados test_*onde implementaremos os cenários de testes que precisamos.
Executando os testes
Introdução 3
# Todos os testes do projeto $ ./manage.py test
# Todos os testes da aplicação "reminder"
$ ./manage.py test reminder
# Todos os testes de um TestCase específico
$ ./manage.py test reminder.tests.test_views.ViewTestCase # Apenas um teste
$ ./manage.py test reminder.tests.test_views.ViewTestCase.test_home
Dica 1
Durante o desenvolvimento de uma funcionalidade não é necessário executar o conjunto completo de testes da sua aplicação o tempo todo. Execute apenas aqueles testes relacionados diretamente com o que você está trabalhando. Deixe para executar todos os testes apenas quando você terminar a sua sessão de trabalho e antes de fazer commit/push para se certificar de que nada esteja quebrado.
Dica 2
Crie um ambiente de integração continua (Continuous Integration - CI) que execute todos os testes do seu projeto sempre que alguém enviar código novo para o respositório de código. Esse tipo de ferramenta é extremamente útil.
Tipos deTestCase
A distribuição padrão do Django disponibiliza vários tipos de classes “TestCase” mas as principais são: django.test.TestCase
Essa classe funciona de forma análoga à classeunittest.TestCaseda biblioteca padrão do Python mas adiciona alguns facilitadores:
1. Cliente: uma instância deClient()no atributoself.client. Essa instância permite que a gente simule a execução de requisições HTTP em nossa aplicação.
2. Transações: durante a execução dos testes o Django controla a execução das transações para permitir que cada teste comece sempre em um cenário de banco de dados vazios.
django.test.LiveServerTestCase
Os conjuntos de testes que herdam dessa classe colocam o servidor de desenvolvimento do Django no ar para permitir a execução de testes de interação com ferramentas para testes funcionais comoSeleniumou Splinter.
Esse tipo de teste é bem difícil de manter e, com o tempo, eles passam a ser algo que mais atrapalha do que ajuda. Eu evito esse tipo de teste porque minhas aplicações geralmente não fazem uso muito intenso de engenharia de frontend (HTML/CSS/JS). Se o seu projeto usa muito JS, por exemplo, esse tipo de teste ganha mais importância.
Introdução 4
O Projeto
Para exemplificar o desenvolvimento guiado por testes vamor criar uma aplicação de agenda/calendário (calendar) em um projeto de projeto de PIM (Personal Information Manager - Gerenciador de Informações Pessoais). Essa aplicação deve ter as seguintes características:
• Gerenciar Events (eventos) para determinado dia com hora de início e fim; • Um evento precisa começar e terminar no mesmo dia;
• Se a hora de início e de término do evento não for informada ele terá duração de um dia inteiro; • Não podemos permitir a criação de mais de um evento para o mesmo horário;
• O serviço precisa ser multiusuário (uma agenda por usuário); • Um usuário não poderá ver a agenda de outro;
• O usuário receberá um email e um SMS (via Twilio) com a antecedência informada no evento. Iniciando
Assumindo que você já tenha um ambiente virtual com Python 3 (venv) e com o Django 1.7 instalado. Vamos iniciar o nosso projeto:
$ django-admin.py startproject pim $ cd pim
$ ./manage.py startapp cal $ mkdir cal/templates/cal/
Nosso projeto deve ter uma estrutura parecida com essa: pim/ |-- cal/ | |-- migrations/ | | `-- __init__.py | |-- templates/ | | `-- cal/ | |-- __init__.py | |-- admin.py | |-- models.py | |-- tests.py <-- Testes | `-- views.py |-- pim/ | |-- __pycache__/ | |-- __init__.py | |-- settings.py | |-- urls.py | `-- wsgi.py `-- manage.py*
O comando./manage.py startappjá cria um módulotests.pymas eu prefiro organizar meus testes de um modo que me permite separar um pouco mais as coisas
Introdução 5
$ mkdir cal/tests/
$ touch cal/tests/__init__.py $ rm cal/tests.py
A aplicaçãocalvai ficar assim: cal/ |-- migrations/ | `-- __init__.py |-- templates/ | `-- cal/ |-- tests/ | `-- __init__.py |-- __init__.py |-- admin.py |-- models.py `-- views.py
Primeiro Teste
Agora que temos a estrutura básica do nosso projeto vamos fazer um teste bem básico: verificar se a página inicial está funcionando.
Vamos criar o primeiro teste: # cal/tests/test_views.py
from django.test import TestCase
class ViewTestCase(TestCase):
def test_home(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200) # 200 OK
E adicionaremos a aplicação calna nossa lista de aplicações instaladas (INSTALLED_APPS) no arquivo pim/settings.py: # pim/settings.py INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles',
Introdução 6
# Project applications
'cal', )
Agora estamos prontos para executar o nosso teste: $ ./manage.py test
Creating test database for alias 'default'... F
====================================================================== FAIL: test_home (cal.tests.test_views.ViewTestCase)
---Traceback (most recent call last):
File "./pim/cal/tests/test_views.py", line 8, in test_home self.assertEqual(response.status_code, 200) # 200 OK AssertionError: 404 != 200
---Ran 1 test in 0.035s
FAILED (failures=1)
Destroying test database for alias 'default'...
E como já era esperado o nosso teste está falhando, afinal, ainda não criamos a view nem roteamos a URL para nossa páginahome. Vamos implementar a nossa view:
# cal/views.py
from django.shortcuts import render
def home(request):
return render(request, "cal/home.html")
Criar um template para ser renderizado: <!-- cal/templates/cal/home.html -->
<html> <head>
<title>Hello World!</title> </head>
<body>
<h1>Hello Django Tests!</h1> </body>
</html>
Introdução 7
# pim/urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin urlpatterns = patterns('',
url(r'^$', 'cal.views.home', name='home'), # / -> home url(r'^admin/', include(admin.site.urls)),
)
Pronto. Agora podemos executar o nosso teste novamente: $ ./manage.py test
Creating test database for alias 'default'... .
---Ran 1 test in 0.011s
OK
Destroying test database for alias 'default'... E tudo passou perfeitamente. Estamos prontos para continuar. Conclusão
Quando a gente trabalha com Test-Driven Development esses passos se repetem o tempo todo: 1. Criamos um teste;
2. Executamos para garantir que o teste falha; 3. Implementamos o código para que o teste passe;
4. Refatoramos o código garantindo que os testes continuem passando.
Dica
Se você criou um teste novo e ele passa de primeira desconfie que ele está testando algo que já foi testado ou que a implementação do teste está incorreta.