Testes automatizados:
--------------------------------------------------------------------
O que são? Para que servem? É realmente útil? Como fazer?
São rotinas simples que checam o funcionamento do código.
Atuam em diferentes níveis, desde o model (verifica se o método retorna valor esperado), até o funcionamento global da aplicação (sequencias de entradas do usuário produz o resultado desejado).
São automatizados porque a própria app faz os testes para o programador. O programador cria os testes apenas uma vez, pode modificá-los se necessário, e os utiliza para checar o funcionamento do código, sem ter que gastar tempo executando testes manuais.
Testes são úteis para ganhar tempo em retestes. Verificar o que parece funcionar será sempre importante. Em uma grande aplicação será um grande ganho de tempo. Após uma alteração importantes de um componente, é importante executar testes para avaliação. Geralmente, os testes automatizados fazem dezenas de testes em segundos. Caso algo esteja com erros, será fácil identificar a origem.
Escrever testes pode ser uma tarefa monótona quando sabemos que está tudo funcionando corretamente. Mas é necessário, para evitar tempo gasto com testes.
Sem testes, o objetivo ou comportamento desejado de uma aplicação pode não ser tão claro.
Testes mudam isso; eles mostram seu código por dentro, e quando algo sai errado, eles revelam parte do erro.
Testes fazem seu código mais atraente: Você pode criar uma parte importante no software, mas você vai encontrar muitos outros desenvolvedores que vão simplesmente ignorar ao olhar isto, por que isto carece de testes, e sem testes, não há confiança.
Assim, esses outros desenvolvedores procuram ver os testes do seu software antes de levar a sério, e isso é uma outra razão para você iniciar a escrita dos testes.
Testes ajudam as equipes a trabalharem juntos: Os pontos anteriores foram escritos do ponto de vista de um único desenvolvedor mantendo uma aplicação. Aplicações complexas serão mantidas por times. Testes garantem que seus colegas de trabalho não quebrem acidentalmente o seu código (e que você não quebre o deles sem saber). Se você deseja trabalhar como um programador Django, deve ser bom escrevendo testes.
--------------------------------------------------------------------
Estratégias básicas de testes:
Há várias maneiras de abordar a escrita de testes.
Existe a disciplina chamada 'test-driven development' (TDD): é uma forma de programar onde o programador codifica testes antes de escrever o código. Isso talvez pareça não intuitivo, mas na verdade é similar o que muitas pessoas farão de qualquer forma: eles descrevem um problema, então criam um código para resolver isso. O desenvolvimento Test-driven simplesmente formaliza o problema em um caso de teste em Python.
Comumente, um novato em testes irá criar algum código e depois decidir que ele deveria ser testado. Talvez seria melhor escrever alguns testes antes, mas nunca é tarde para começar.
--------------------------------------------------------------------
Codificando o 1º teste automatizado:
Felizmente, há um pequeno bug na aplicação votacao para corrigirmos imediatamente: O método Questao.was_published_recently() retorna True se a Questao foi publicada dentro do último dia (o que está correto), mas também se o campo data_pub de Questao está no futuro (o que certamente não é o caso).
Para checar se o bug realmente existe, usando o Admin crie uma enquete na qual a data encontra-se no futuro e verifique o método através do shell (de dentro do diretório da app):
python manage.py shell
Digite:
import datetime
from django.utils import timezone
from votacao.models import Questao
#cria future_question com data 30 dias no futuro
future_question = Questao(data_pub=timezone.now() + datetime.timedelta(days=30))
#future_question foi publicada recentemente?
future_question.was_published_recently()
#Resultado na tela: True
Observe que se a data foi de 30 dias no futuro, não pode ter sido publicada recentemente. Isto é claramente um erro.
--------------------------------------------------------------------
Criando um teste:
O que fizemos acima no shell para testar o problema é exatamente o que podemos fazer em um teste automatizado.
O lugar padrão para os testes de uma app é o arquivo tests.py.
O sistema de testes irá encontrar automaticamente todos os testes que estiverem em qualquer arquivo cujo o nome comece com test.
Coloque o seguinte código no arquivo tests.py na aplicação votacao:
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Questao
class QuestaoModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() retorna False para questoes que a data_pub está no futuro.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Questao(data_pub=time)
self.assertIs(future_question.was_published_recently(), False)
Criamos uma subclasse django.test.TestCase com o método que cria uma instância de Questiao com data_pub no futuro. Vamos checar a saída de was_published_recently(): que deve ser 'False'.
--------------------------------------------------------------------
Executando o teste:
No terminal, nós podemos executar nosso test:
$ python manage.py test votacao
E você verá algo como:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
====================================================================
FAIL: test_was_published_recently_with_future_question (votacao.tests.QuestaoModelTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/enquetes/tests.py", line 21, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
--------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
--------------------------------------------------------------------
O que ocorreu foi o seguinte:
O comando 'python manage.py test votacao' procurou por testes na aplicação votacao.
Encontrou uma subclasse da classe django.test.TestCase.
Criou um banco de dados especial com o propósito de teste.
Procurou por métodos de test - aqueles cujos nomes começam com test.
Em test_was_published_recently_with_future_question é criado uma instância de Questao, definindo o campo data_pub para estar com 30 dias no futuro.
Usou o método assertIs(), para descobrir que o método was_published_recently() retorna True, mas queremos que retorne False.
O teste nos informa que o teste falhou e até mesmo a linha na qual a falha ocorreu.
--------------------------------------------------------------------
Corrigindo o erro:
Já sabemos onde está o problema.
'Questao.was_published_recently()' deveria retornar 'False' se 'data_pub' está no futuro.
Vamos alterar o método em models.py, dessa forma ele só retornará 'True' se a data também estiver no passado:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.data_pub <= now
Execute o teste novamente: python manage.py test votacao
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
--------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
--------------------------------------------------------------------
Depois de identificar um erro, nós escrevemos um teste que demonstra e corrige o erro no código, então o nosso teste passa.
Muitas outras coisas podem dar errado com a nossa aplicação no futuro, mas você pode ter certeza que não vamos reintroduzir este bug inadvertidamente, porque simplesmente rodando os testes já receberemos o aviso imediatamente. Nós podemos considerar que esta pequena parte da aplicação está a salvo para sempre.
--------------------------------------------------------------------
Testes mais abrangentes:
Vamos aprofundar nos testes do método was_published_recently().
Adicione mais 2 métodos de teste na mesma classe.
Codifique: votacao/tests.py
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() retorna False para questoes que data_pub é mais antiga que 1 dia.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Questao(data_pub=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() retorna True para questoes que data_pub esteja no último dia.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Questao(data_pub=time)
self.assertIs(recent_question.was_published_recently(), True)
Execute no prompt: python manage.py test votacao
Foram executados 3 testes.
Temos então, 3 testes que confirmam que was_published_recently() retorna valores sensíveis de perguntas do passado, recente e futuro.
Agora temos alguma garantia que o método testado acima vai se comportar de maneira esperada, dentro dos testes.
A lista de enquete mostra enquetes que não estão publicadas ainda (isto é aqueles que tem data_pub no futuro). Vamos consertar isso.Vamos aprofundar nos testes do método was_published_recently().
Adicione mais 2 métodos de teste na mesma classe.
Codifique: votacao/tests.py
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() retorna False para questoes que data_pub é mais antiga que 1 dia.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Questao(data_pub=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() retorna True para questoes que data_pub esteja no último dia.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Questao(data_pub=time)
self.assertIs(recent_question.was_published_recently(), True)
Execute no prompt: python manage.py test votacao
Foram executados 3 testes.
Temos então, 3 testes que confirmam que was_published_recently() retorna valores sensíveis de perguntas do passado, recente e futuro.
Agora temos alguma garantia que o método testado acima vai se comportar de maneira esperada, dentro dos testes.
--------------------------------------------------------------------
Testando uma View
Atualmente, a app enquete irá publicar qualquer questão, inclusive as que o campo data_pub esteja no futuro. Vamos melhorar isso: Definindo um data_pub no futuro deveria fazer com que a Questao seja publicada, mas que esteja invisível até lá.
No 1º teste, focamos no comportamento interno do código. Para este teste, queremos checar seu comportamento como seria experienciado por um usuário através de um navegador web.
o Django fornece um 'test Client' para simular um usuário interagindo com o código no nível de view. Podemos usá-lo em tests.py ou no shell.
Começaremos com o shell, onde faremos algumas coisas que não seriam necessárias no tests.py. A 1ª é definir o ambiente de teste no django:shell:
from django.test.utils import setup_test_environment
setup_test_environment()
O setup_test_environment() instala um renderizador de templates o qual permite que sejam examinados alguns atributos adicionais nas respostas http tal como um response.context que de outra maneira não estariam disponíveis. Este método não configura um banco de dados de teste, o que se segue será executado no banco de dados existente e a saída pode ser diferente quanto as questões que você já criou. Você talvez tenha resultados inesperados se seu TIME_ZONE no settings.py não estiver correto. Se você não lembrou de defini-lo antes, verifique-o antes de continuar.
Agora precisamos importar a classe cliente de teste (depois iremos usar a classe django.test.TestCase no tests.py, a qual traz seu próprio cliente, então isso não será necessário):
from django.test import Client
# cria uma instancia de Client para nosso uso
client = Client()
Agora, podemos pedir que o cliente faça algum trabalho para a gente:
# busque um response de '/'
response = client.get('/')
#Saída: Not Found: /
# devemos esperar um 404 deste endereço; se em vez de ver um erro "Invalid HTTP_HOST header" ver um 400 response, você provavelmente omitiu a chamada de setup_test_environment() descrita acima.
response.status_code
#Saída: 404
# De outra forma, devemos esperar encontarr algo como '/votacao/'
# Nós usaremos 'reverse()' em vez de um URL codificado
from django.urls import reverse
response = client.get(reverse('votacao:index'))
response.status_code
#Saída: 200
response.content
#Saída: "b'\n\n\n <ul>\n\n \n\n\t\t<!--<li><a href="/votacao/1/">Novidade?</a></li>
-->\n <li><a href="/votacao/1/">Novidade?</a></li>\n\t\t\n \n\n\t\t<!
--<li><a href="/votacao/2/">Nova Quest\xc3\xa3o</a></li> -->\n <li><a hre
f="/votacao/2/">Nova Quest\xc3\xa3o</a></li>\n\t\t\n \n\n </ul>\n\n'"
--------------------------------------------------------------------
Melhorando a nossa view:
Antes, introduzimos o class-based views, baseado no ListView:
votacao/views.py
class IndexView(generic.ListView):
template_name = 'votacao/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-data_pub')[:5]
Precisamos fazer uma adição no método get_queryset() e alterá-lo de modo que ele verifique também a data comparando com timezone.now().
Codificando votacao/views.py:
...
from django.utils import timezone
...
def get_queryset(self):
"""Retorna as últimas 5 questões publicadas."""
#return Questao.objects.order_by('-data_pub')[:5]
"""Return the last five published questions (not including those set to be published in the future)."""
return Questao.objects.filter(data_pub__lte=timezone.now()).order_by('-data_pub')[:5]
Questao.objects.filter(data_pub__lte=timezone.now()): retorna um queryset contendo Questoes cujo o data_pub é menor ou igual a - que quer dizer, anterior ou igual a - timezone.now.
--------------------------------------------------------------------
Observações:
Queries:
>/GREATER THAN e </LESS THAN: gt, gte, lt, lte
As instruções SQL WHERE associadas a campos numéricos geralmente usam operadores matemáticos >, >=, =, <, <=, para restringir consultas a um determinado intervalo de números. Os modelos do Django suportam o uso de operadores matemáticos como segue:
- > : gt
- >= : gte
- < : lt
- <= : lte
from coffeehouse.items.models import Item
# Get Item records with stock > 5
Item.objects.filter(stock__gt=5)
# Get Item records with stock > or equal 10
Item.objects.filter(stock__gte=10)
# Get Item records with stock < 100
Item.objects.filter(stock__lt=100)
# Get Item records with stock < or equal 50
Item.objects.filter(stock__lte=50)
--------------------------------------------------------------------
Testando a nossa nova view:
Vamos criar um teste, baseado na sessão shell acima; Crie uma função de atalho para criar perguntas assim como uma nova classe de teste. Codifique:
...
from django.urls import reverse
def create_question(texto_questao, days):
"""
Cria uma questao com o dado `texto_questao` e publique-o com
determinado número de dias de compensação até agora (negativo para questões publicadas
no passado, positivas para questões que ainda não foram publicadas).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Questao.objects.create(texto_questao=texto_questao, data_pub=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
Se nenhuma questão existe, uma mensagem apropriada é exibida.
"""
response = self.client.get(reverse('votacao:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Nenhuma votacao está disponível.")
self.assertQuerysetEqual(response.context['latest_questao_list'], [])
def test_past_question(self):
"""
Questoes com o data_pub no passado são mostradas na página index.
"""
create_question(texto_questao="Past question.", days=-30)
response = self.client.get(reverse('votacao:index'))
self.assertQuerysetEqual(
response.context['latest_questao_list'],
['<Questao: Past question.>']
)
def test_future_question(self):
"""
Questões com o data_pub no futuro não são mostradas na página index.
"""
create_question(texto_questao="Future question.", days=30)
response = self.client.get(reverse('votacao:index'))
self.assertContains(response, "Nenhuma votacao está disponível.")
self.assertQuerysetEqual(response.context['latest_questao_list'], [])
def test_future_question_and_past_question(self):
"""
Mesmo que existam questões no passado e no futuro, apenas perguntas no passado são exibidas.
"""
create_question(texto_questao="Past question.", days=-30)
create_question(texto_questao="Future question.", days=30)
response = self.client.get(reverse('votacao:index'))
self.assertQuerysetEqual(
response.context['latest_questao_list'],
['<Questao: Past question.>']
)
def test_two_past_questions(self):
"""
As questões na página index podem exibir várias questões.
The questions index page may display multiple questions.
"""
create_question(texto_questao="Past question 1.", days=-30)
create_question(texto_questao="Past question 2.", days=-5)
response = self.client.get(reverse('votacao:index'))
self.assertQuerysetEqual(
response.context['latest_questao_list'],
['<Questao: Past question 2.>', '<Questao: Past question 1.>']
)
--------------------------------------------------------------------
Explicações:
Começamos com uma função de atalho para questão, 'create_question', para retirar algumas repetições do processo de criar questões.
A função 'test_no_questions' não cria nenhuma questão, apenas verifica se precisa exibir a mensagem: 'Nenhuma votação está disponível', e verifica se 'latest_question_list' está vazio.
Note que a classe django.test.TestCase fornece alguns métodos adicionais de assertion.
Nestes exemplos, usamos assertContains() e assertQuerysetEqual().
Em test_past_question, nós criamos uma questão e verificamos o que aparece na lista.
Em test_future_question, nós criamos uma questão com o data_pub no futuro.
O banco de dados é resetado para cada método de teste. Então a primeira questão não está mais lá, e então novo a página index não deve exibir nenhuma questão nela.
E assim por diante. De fato, estamos usando os testes para contar uma história da entrada do admin e da experiencia do usuário no site, e verificando se para cada estado e para cada nova alteração no estado do sistema, o resultado esperado é publicado.
--------------------------------------------------------------------
Testando o DetailView:
Mesmo quando questões futuras não aparecem no index, os usuários ainda podem acessá-las se eles conhecem ou adivinham a URL correta. Então precisamos adicionar uma regra similar ao DetailView em votacao/views.py:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Exclui qualquer questão que nao foi publicada ainda..
"""
return Questao.objects.filter(data_pub__lte=timezone.now())
Agora, adicionaremos mais testes para verificar que uma Questao, a qual seu data_pub está no passado pode ser mostrada, e que uma com data_pub no futuro não pode. Codifique votacao/tests.py:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
O detail view de uma questao com o data_pub no futuro retorna um 404 (not found).
"""
future_question = create_question(texto_questao='Future question.', days=5)
url = reverse('votacao:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
O detail view de uma questao com o data_pub no passado mostra o texto da questao.
"""
past_question = create_question(texto_questao='Past Question.', days=-5)
url = reverse('votacao:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.texto_questao)
--------------------------------------------------------------------
Ideias para mais testes:
Devemos adicionar um método similar ao get_queryset ao ResultsView e criar uma nova classe de teste para essa view. Isso será muito parecido com o que criamos há pouco; defato terá muitas repetições.
Podemos também melhorar nossa aplicação de várias outras maneiras, adicionando testes no caminho. Por exemplo é estranho que Questoes possam ser publicadas no site sem que tenham Escolhas. Então, nossas views podem verificar isso, e excluir tais Questoes. Nossos testes podem criar uma Questao sem Escolha e então testar que não estão publicadas, também criar uma Questao parecida com Escolhas, e testar se estão publicadas.
Talvez usuários registrados no admin devam ser permitidos a ver Questoes não publicadas, mas não os visitantes comuns. Independente da necessidade a ser adicionada para que isso seja realizado, deve ser acompanhada por um teste, não importando se você escreve o teste primeiro e faz o código para passar no teste, ou escreva o código da lógica primeiro e escreva o teste para fazer a prova.
--------------------------------------------------------------------
Quando estiver testando, mais é melhor:
Pode parecer que nossos testes estão crescendo fora de controle. Nessa taxa teremos logo mais código nos testes do que na aplicação, e a repetição não é muito estética, comparado a concisa elegância do resto do código.
Isso não importa. Deixe eles crescerem. Por boa parte do tempo, você pode escrever um teste uma vez e então esquecer dele. Ele vai continuar a executar sua função útil enquanto você continua a escrever a app.
Algumas vezes os testes precisarão ser atualizados. Suponha que emendamos as views para que somente Questoes com Escolhas são publicadas. Neste caso, muitos dos nossos testes existentes irão falhar - dizendo exatamente quais testes precisam ter ajustes para que sejam atualizados, assim que os próprios testes auxiliam na manutenção deles mesmos.
Na pior das hipóteses, ao continuar a desenvolver, talvez encontre alguns testes que são redundantes agora. Mas isso não é problema; em testes redundância é uma boa coisa.
Boas práticas para codificação de testes:
• um 'TestClass' separado para cada model ou view
• um método de teste separado para cada conjunto de condições que deseja testar
• nomes de métodos de teste que descrevem a sua função
Mais testes:
Há muito mais do que você pode fazer, e uma série de ferramentas muito úteis à disposição para conseguir algumas coisas inteligentes.
Por exemplo, enquanto nossos testes aqui tem coberto algumas lógicas internas de um model e a informação publicada nas views, você pode usar um framework para browsers como o Selenium para testar a maneira como seu HTML é processado de verdade pelo browser. Isso permite testar não somente o comportamento do código Django, mas também, por exemplo, do código javascript. É bem legal ver seus testes iniciarem um navegador, e começar a interagir com seu site, como se fosse um ser humano guiando!
O Django inclui a LiveServerTestCase para facilitar a integração com ferramentas como o Selenium.
Se você tem uma aplicação complexa, você pode querer rodar testes automaticamente com cada commit pelo propósito de integração contínua, e o próprio controle de qualidade - ao menos parcialmente - é automatizado.
Uma boa forma de encontrar partes não testadas de sua aplicação é verificar a cobertura de código. Isto também ajuda a identificar códigos frágeis ou mesmo morto. Se você não pode testar um pedaço do código, isso normalmente significa que o código precisa ser refatorado ou removido.
--------------------------------------------------------------------
Nenhum comentário:
Postar um comentário