Escrevendo sua primeira aplicação Django, parte 5¶
Este tutorial começa onde o Tutorial 4 parou. Nós construímos uma aplicação web de enquete, e agora nós vamos criar alguns testes automatizados para ela.
Onde obter ajuda:
Se tiver problemas enquanto caminha por este tutorial, por favor consulte a seção Obtendo ajuda da FAQ.
Apresentando testes automatizados¶
O que são testes automatizados?¶
Testes são rotinas que verificam o funcionamento do seu código.
Testes funcionam em diferentes níveis. Alguns testes podem ser aplicados a um pequeno detalhe (um determinado método de um modelo retorna o valor esperado?) enquanto outros examinam o funcionamento global do software (a sequência de entradas do usuário no site produz o resultado desejado?). Isso não é diferente do teste que você fez anteriormente no Tutorial 2, usando o shell
para examinar o comportamento de um método, ou executar a aplicação e entrar com um dado para verificar como ele se comporta.
A diferença em testes automatizados é que o trabalho de testar é feito para você pelo sistema. Você cria um conjunto de testes uma vez, e então conforme faz mudanças em sua aplicação, você pode checar se o seu código continua funcionando como você originalmente pretendia, sem ter que gastar tempo executando o teste manualmente.
Porque você precisa criar testes¶
Então, por que criar testes, e por que agora?
Você pode achar que já tem coisa suficiente no seu prato apenas aprendendo Python/Django, e tendo ainda outra coisa para aprender e fazer pode parecer exagerado e talvez desnecessário. Afinal, nossa aplicação de enquetes está funcionando muito bem agora; passar pelo trabalho de criar testes automatizados não vai fazê-la funcionar melhor. Se criar a aplicação de enquetes é a última coisa que você vai programar em Django, então realmente, você não precisa saber sobre como criar testes automatizados. Mas, se esse não é o caso, agora é uma excelente hora para aprender.
Teste vão salvar seu tempo¶
Até um certo ponto, ‘verificar que parece funcionar’ será um teste satisfatório. Em uma aplicação mais sofisticada, você pode ter dúzias de interações complexas entre componentes.
A change in any of those components could have unexpected consequences on the application’s behavior. Checking that it still ‘seems to work’ could mean running through your code’s functionality with twenty different variations of your test data to make sure you haven’t broken something - not a good use of your time.
Isso é verdade especialmente quando testes automatizados poderiam fazer isso pra você em segundos. Se alguma coisa errada acontecer, os testes também ajudarão em identificar o código que está causando o comportamento inesperado.
Às vezes pode parece uma tarefa para afastar você da sua produtividade e criatividade em programar para enfrentar o trabalho nada excitante e glamouroso de escrever testes, particularmente quando você sabe que seu código está funcionando corretamente.
No entanto, a tarefa de escrever testes é muito mais satisfatório do que gastar horas testando sua aplicação manualmente, ou tentando identificar a causa de um problema recém-introduzido.
Testes não só identificam problemas, mas também os previnem¶
É um erro pensar nos testes simplesmente como um aspecto negativo do desenvolvimento.
Sem testes, o objetivo ou comportamento desejado de uma aplicação pode não ser tão claro. Mesmo quando é seu próprio código, algumas vezes você vai se encontrar bisbilhotando nele tentando descobrir exatamente o que está fazendo.
Testes mudam isso; eles mostram seu código por dentro, e quando algumas coisas saem errado, eles revelam parte do erro.
Testes fazem seu código mais atraente.¶
You might have created a brilliant piece of software, but you will find that many other developers will refuse to look at it because it lacks tests; without tests, they won’t trust it. Jacob Kaplan-Moss, one of Django’s original developers, says “Code without tests is broken by design.”
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.
Some programmers follow a discipline called “test-driven development”; they actually write their tests before they write their code. This might seem counter-intuitive, but in fact it’s similar to what most people will often do anyway: they describe a problem, then create some code to solve it. Test-driven development formalizes the problem in a Python test case.
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.
Às vezes é difícil saber por onde devemos começar a escrever testes. Se você tiver escrito milhares de linhas em Python, escolher algo para testar pode não ser fácil. Nesse caso, é interessante escrever seu primeiro teste na próxima vez que você fizer alguma alteração, seja a adição de uma nova função ou correção de um bug.
Então vamos fazer isso a partir de agora.
Escrevendo nosso primeiro teste¶
Nós identificamos um bug¶
Felizmente, há um pequeno bug na aplicação polls
para corrigirmos imediatamente: O método Question.was_published_recently()
retorna True
se a Question
foi publicada dentro do último dia (o que está correto), mas também se o campo pub_date
de Question
está no futuro (o que certamente não é o caso).
Confirme o bug utilizando shell
para verificar o método numa questão em que a data encontra-se no futuro:
$ python manage.py shell
...\> py manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
Desde que coisas no futuro não são ‘recent’, isto é claramente um erro.
Criar um teste para expor um bug¶
O que nós fizemos no shell
para testar o problema é exatamente o que podemos fazer em um teste automatizado, então vamos voltar para o teste automatizado
Um lugar convencional para os testes da aplicação é 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 polls
:
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
Aqui criamos uma subclasse de django.test.TestCase
com um método que cria uma instância de Question
com uma pub_date
no futuro. Então verificamos a saída de was_published_recently()
- o qual deve ser False.
Executando o teste¶
No terminal, podemos rodar nosso teste:
$ python manage.py test polls
...\> py manage.py test polls
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 (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, 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'...
Erro diferente?
Se no lugar disso, você recebeu uma exceção NameError
, você pode ter pulado algo passo da Parte 2 onde nós adicionamos “imports” de``datetime`` e timezone
em polls/models.py
. Copie os “imports” dessa seção, e tente executar seus testes novamente.
O que ocorreu foi o seguinte:
manage.py test polls
looked for tests in thepolls
application- ele encontrou uma subclass da classe
django.test.TestCase
- ele cria um banco de dados especial com o propósito de teste
- ele procurou por métodos de test - aquele cujo nome começam com
test
- em
test_was_published_recently_with_future_question
é criado uma instância deQuestion
na qual o campopub_date
está 30 dias no futuro - … e usando o método
assertIs()
, descobrimos quewas_published_recently()
retornaTrue
, mas queremos que retorneFalse
O teste nos informa que teste falhou e até mesmo a linha na qual a falha ocorreu.
Corrigindo o erro¶
Nós sabemos onde o problema está. “Question.was_published_recently()” deveria retornar “False” se “pub_date” está no futuro. Corrija o método no “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.pub_date <= now
execute e teste novamente
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.
Many other things might go wrong with our application in the future, but we can be sure that we won’t inadvertently reintroduce this bug, because running the test will warn us immediately. We can consider this little portion of the application pinned down safely forever.
Testes mais abrangentes¶
Enquanto estamos aqui, nós podemos nos aprofundar no método was_published_recently()
; De fato, seria certamente embaraçoso introduzir um novo bug enquanto arruma outro.
Adicione mais dois métodos de teste na mesma classe, para testá-la melhor.
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
E agora nós temos 3 testes que confirmam que Question.was_published_recently()
retorna valores sensíveis de perguntas do passado, recente e futuro.
Again, polls
is a minimal application, but however complex it grows in the
future and whatever other code it interacts with, we now have some guarantee
that the method we have written tests for will behave in expected ways.
Teste a View¶
A aplicação de enquete é bastante indiscriminatória: irá publicar qualquer questão, incluindo aquelas que o campo pub_date
está no futuro. Devemos melhorar isso. Definindo um pub_date
no futuro deveria fazer com que a Question seja publicada naquele momento, mas que esteja invisível até lá.
Um teste para uma view¶
When we fixed the bug above, we wrote the test first and then the code to fix it. In fact that was an example of test-driven development, but it doesn’t really matter in which order we do the work.
Em nosso primeiro teste, focamos no comportamento interno do código. Para este teste, nós queremos checar seu comportamento como seria experienciado por um usuário através de um navegador web.
Antes de tentarmos consertar alguma coisa, vamos dar uma olhada nas ferramentas à nossa disposição.
O cliente de testes do Django¶
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 memso no shell
.
We will start again with the shell
, where we need to do a couple of
things that won’t be necessary in tests.py
. The first is to set up the test
environment in the shell
:
$ python manage.py shell
...\> py manage.py 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. Note que este método não configura um banco de dados de teste, então 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.
A diante 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
>>> # create an instance of the client for our use
>>> client = Client()
Com isso pronto, podemos pedir que o cliente faça algum trabalho para a gente:
>>> # get a response from '/'
>>> response = client.get('/')
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
Melhorando a nossa view¶
A lista de enquete mostra enquetes que não estão publicadas ainda (isto é aqueles que tem um ``pub_date``no futuro). Vamos concertar isso.
No Tutorial 4 nós introduzimos o class-based views, baseado no ListView
:
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[: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()
. Antes precisamos adicionar uma importação:
from django.utils import timezone
e então devemos fazer uma adição ao método get_queryset
como a seguir:
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
Question.objects.filter(pub_date__lte=timezone.now())
retorna um queryset contendo Question
s cujo o pub_date
é menor ou igual a - que quer dizer, anterior ou igual a - timezone.now
.
Testando nossa nova view¶
Agora você pode se certificar se isso se comporta como esperado executando runserver
, carregando o site no seu navegador, criando Questions
com datas no passado e no futuro, e verificando que somente aqueles que foram publicados são listados. Você não quer ter que fazer isso toda vez que fizer uma alterações que possa afetar isso - então vamos também criar um teste, baseado no nossa sessão shell
acima.
Adicione o seguinte a “polls/tests.py”:
from django.urls import reverse
e nós vamos criar uma função de atalho para criar perguntas assim como uma nova classe de teste:
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question2, question1],
)
Vejamos algumas delas mais de perto.
O primeiro é uma função de atalho para questão, create_question
, para retirar algumas repetições do processo de criar questões.
test_no_questions
não cria nenhuma questão, mas verifica a mensagem:”Nenhuma enquete disponível.” e verifica que latest_question_list
está vazia. Note que a classe django.test.TestCase
fornece alguns métodos adicionais de assertividade. Nestes exemplos nós usamos assertContains()
e assertQuerysetEqual()
.
Em test_past_question
, criamos uma questão e verificamos se esta aparece na lista.
Em test_future_question
, nós criamos uma pub_date
no futuro. O banco de dados é resetado para cada método de teste, quer dizer que a primeira questão não está mais lá, então novamente o index não deve ter nenhuma questão.
E assim por diante. De fato, estamos usando os testes para contar uma história de entradas no site de administração e da experiencia do usuário, e verificando se para cada estado e para cada nova alteração no estado do sistema, o resultado esperado é publicado.
Testando o DetailView
¶
O que temos funciona bem; Porém, 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
:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
We should then add some tests, to check that a Question
whose pub_date
is in the past can be displayed, and that one with a pub_date
in the future
is not:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
The detail view of a question with a pub_date in the future
returns a 404 not found.
"""
future_question = create_question(question_text='Future question.', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
The detail view of a question with a pub_date in the past
displays the question's text.
"""
past_question = create_question(question_text='Past Question.', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
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 Questions
possam ser publicadas no site sem que tenham Choices
. Então, nossas “views” podem verificar isso, e excluir tais Questions
. Nossos testes podem criar uma Question
sem Choices
e então testar que não estão publicadas, também criar uma Question
parecida com Choices
, e testar se estão publicadas.
Talvez usuários registrados no admin devam ser permitidos a ver Questions
não publicadas, mas não os visitantes comuns. Denovo: independente da necessidade a serem adicionadas ao software para que isso seja realizado, isso deve ser acompanhado por um teste, não importando se você escreve o teste primeiro e então faz o código para passar no teste, ou escreva o código da lógica primeiro e então escreva o teste para fazer a prova.
Em um certo ponto, você precisará olhar seus testes e se perguntar se o seu código está sofrendo de inchaço de testes, o que nos trás a:
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 em nossa aplicação, e a repetição não é muito estética, comparado a concisa elegância do resto do nosso 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 seu programa.
Algumas vezes os testes precisarão ser atualizados. Suponha que emendamos nossas “views” para que somente Questions``com ``Choices
são publicadas. Neste caso, muitos dos nossos testes existentes irão falahar - nos 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, você talvez encontre alguns tests que são reduntantes agora. Mesmo isso não é um problema; em testes redundância é uma boa coisa.
Enquanto seus testes estiverem organizados sensatamente, eles não vão se tornar desorganizados. Boas práticas incluem ter:
- um “TestClass” separado para cada modelo ou view
- um método de teste separado para cada conjunto de condições que você quer testar
- nomes de métodos de teste que descrevem a sua função
Mais testes¶
Esse tutorial apenas introduz alguns conceitos básicos de testes. Há muito mais do que você pode fazer, e uma série de ferramentas muito úteis à sua disposição para conseguir algumas coisas muito inteligentes.
Por exemplo, enquanto nossos testes aqui tem coberto algumas lógicas internas de um modelo e a informação publicada nas nossas “views”, você pode usar um framework para navegador como o Selenium para testar a maneira como seu HTML é processado de verdade pelo navegador. Isso permite a você testar não somente o comportamento do seu código Django, mas também, por exemplo, do seu 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 inclue 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. Cobertura vai ajudar a identificar código morto. Veja Integration with coverage.py para mais detalhes.
Testes no django tem informações sobre testes.
E agora?¶
Para maiores detalhes sobre testing, veja Testando no Django.
Quando estiver confortável testando “views” Django, leia: doc:parte 6 deste tutorial</intro/tutorial06> para aprender sobre gerenciamente de arquivos estáticos.