Scrivere la tua prima applicazione in Django, parte 5¶
Questo tutorial inizia dove è terminato Tutorial 4 . Abbiamo costruito una applicazione web per i questionari ed adesso per essa creeremo alcuni testi automatici.
Dove trovare aiuto:
Se hai difficoltà a completare questo tutorial, per favore vai alla sezione Getting Help delle FAQ.
Introduzione ai test automatici¶
Cosa sono i test automatici?¶
I test sono routine che controllano il funzionamento del codice.
Il testing opera a livelli differenti. Alcuni test si posso applicare a piccoli dettagli (un modello particolare restituisce i valori come ci si aspetta?) mentre altri esaminano le operazioni del software a linee più generali (la sequenza di input dell’utente sul sito produce l’output desiderato?). Non è differente dal tipo di test che hai fatto nel Tutorial 2, usando la shell
per esaminare il comportamento di un metodo o lanciando l’applicazione ed inserendo i dati per controllare come si comporta.
La differenza nell” automatizzare i test è che il sistema svolge i test al posto tuo. Tu crei un insieme di test una volta, e ogni volta che effettui una modifica alla tua applicazione, puoi controllare che il codice funzioni come ti aspettavi originariamente, senza dover impiegare tempo a svolgere test manualmente.
Perché devi creare dei test¶
Quindi perche creare test, e perche ora?
Potresti sentire di avere abbastanza da fare solo per imparare Python / Django, e avere ancora un’altra cosa da imparare e da fare può sembrare gravoso e forse non necessario. Dopotutto, la nostra applicazione per i sondaggi ora funziona abbastanza bene; affrontare il problema della creazione di test automatizzati non lo farà funzionare meglio. Se la creazione dell’applicazione sondaggi è l’ultima parte della programmazione Django che farai mai, allora è vero, non hai bisogno di sapere come creare test automatizzati. Ma se non è così, ora è un ottimo momento per imparare.
I test ti faranno risparmiare tempo¶
Fino a un certo punto accertarsi che l’app “sembra funzionare” potrebbe essere un test soddisfacente. In applicazioni sofisticate, potresti avere dozzine di complesse interazioni tra i componenti.
Una modifica in uno di questi componenti potrebbe avere conseguenze inaspettate sul comportamento dell’applicazione. Controllare che «tutto funzioni» potrebbe voler dire provare le funzionalità del tuo codice con decine di differenti varianti dei tuoi dati di test per essere sicuro di non aver danneggiato nulla con l’ultima modifica - non un bell’impiego del tuo tempo.
Specialmente quando i test automatici possono fare tutto questo per te in pochi secondi. Se qualcosa va storto, i test ti assisteranno nel trovare il codice che ha causato il comportamento inaspettato.
A volte, potrebbe sembrare un lavoro che toglie tempo alla tua produttività, dover programmare per affrontare il compito poco affascinante e poco emozionante di scrivere test, soprattutto quando sai che il tuo codice sta funzionando a dovere.
Tuttavia, scrivere test è molto più soddisfacente che passare ore a testare manualmente l’applicazione o cercare di identificare la causa di un problema appena introdotto.
I test non si limitano a identificare i problemi, ma li prevengono¶
È uno sbaglio pensare che i test siano un aspetto negativo dello sviluppo.
Senza test, il fine o il comportamento atteso di una applicazione potrebbe essere piuttosto opaco. Anche se è il tuo codice, talvolta ti ritroverai a frugarci dentro cercando di capire che cosa fa esattamente.
I test operano un cambiamento; illuminano il tuo codice dall’interno e quando qualcosa va storto, gettano luce sulla parte che ha fallito – anche quando non ti sei nemmeno accorto che ha fallito.
I test rendono il tuo codice più attraente¶
Potresti anche aver creato un software brillante ma potresti scoprire che molti altri sviluppatori si rifiutano di dargli uno sguardo perchè manca dei test; senza test, non ci faranno affidamento. Jacob Kaplan-Moss, uno degli sviluppatori originali di Django, dice «Il codice senza test è marcio dalle fondamenta».
Il fatto che altri sviluppatori vogliano vedere i test nel tuo software prima di prenderlo sul serio è ancora un altro motivo per iniziare a scrivere test.
I test aiutano i gruppi a lavorare insieme¶
I punti precedenti sono scritti dal punto di vista di un solo sviluppatore che mantiene una applicazione. Applicazioni complesse saranno manutenute da team. I test garantiscono che i colleghi non rompano inavvertitamente il codice (e che tu non danneggi il loro senza volerlo). Se vuoi vivere della programmazione Django, devi essere bravo a scrivere test!
Strategie basilari di test¶
Ci sono tanti modi per scrivere i test.
Alcuni programmatori seguono una disciplina chiamata «test-driven development»; scrivono test anche prima di scrivere il codice. Può sembrare una cosa controintuitiva ma in effetti è simile a quel che molte persone farebbero in ogni caso; descrivono un problema, poi creano del codice per risolverlo. Il test-driven development formalizza il problema in un test case Python.
Molto più spesso, un principiante del testing creerà un po” di codice e poi deciderà che debba avere dei test. Magari sarebbe stato meglio scrivere alcuni test prima ma non è mai troppo tardi per iniziare.
A volte è difficile capire da dove cominciare per scrivere dei test. Se hai scritto molte centinaia di righe di Python, scegliere qualcosa da testare può non essere semplice. In questi casi, puoi scrivere il tuo primo test la prossima volta che apporti un cambiamento, che sia aggiungere una nuova feature o risolvere un bug.
Quindi facciamo le cose in maniera giusta.
Scrivere il nostro primo test¶
Identifichiamo un bug¶
Fortunatamente, c’è un piccolo bug nell’applicazione polls
che possiamo risolvere: il metodo Question.was_published_recently()
restituisce True
se Question
è stata pubblicata entro un giorno (che è corretto) ma anche se il campo pub_date
di Question
è futuro (che certamente non va bene).
Conferma il bug usando la shell
per controllare il metodo su una domanda la cui data è futura:
$ 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
Siccome le cose nel futuro non sono “recenti”, questo è chiaramente sbagliato.
Creare un test per esporre un bug¶
Quel che abbiamo appena fatto nella shell
per testare il problema è esattamente quel che facciamo nel nostro test automatizzato, quindi convertiamolo in un test automatico.
Il posto che per convenzione ospita i test di una applicazione è il file tests.py
; il sistema di testing troverà automaticamente i test in ogni file il cui nome inizia per test
.
Metti quanto segue nel file tests.py
nell’applicazione 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)
Qui abbiamo creato una sottoclasse django.test.TestCase
con un metodo che crea una istanza di Question
con una pub_date
nel futuro. Quindi controlliamo l’output di was_published_recently()
- che dovrebbe essere False.
Eseguire i test¶
Nel terminale, possiamo eseguire il nostro test:
$ python manage.py test polls
...\> py manage.py test polls
e vedrai qualcosa del genere:
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'...
Errore diverso?
Se al contrario ottieni un NameError
, potresti esserti perso uno step nella Parte 2 dove abbiamo aggiunto gli import di datetime
e timezone
a polls/models.py
. Copia gli import da quella sezione e prova a lanciare di nuovo i test.
Quello che è successo è questo
manage.py test polls
cerca i test nell’applicazionepolls
- ha trovato una sottoclasse di tipo
django.test.TestCase
- ha creato un database speciale con lo scopo di testing
- cerca metodi di test - quelli i cui nomi iniziano con
test
- in
test_was_published_recently_with_future_question
ha creato una istanza diQuestion
il cui campopub_date
è avanti di 30 giorni nel futuro - …ed usando il metodo
assertIs()
, ha scoperto che il suowas_published_recently()
restituisceTrue
, anche se volevamo che restituisseFalse
Il test ci informa su quale test è fallito e ci dice anche la linea su cui si è verificato il fallimento.
Correggere il bug¶
Sappiamo già che il problema è: Question.was_published_recently()
dovrebbe restituire False
se la sua pub_date
è futura. Correggi il metodo in models.py
, così che restiuisca True
solo se la data è nel passato:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
ed eseguire nuovamente il test
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'...
Dopo aver identificato un bug, abbiamo scritto un test che lo evidenzia e corretto il bug nel codice in modo che il nostro test venga superato.
Molte altre cose potrebbero andare storte con la nostra applicazione in futuro, ma possiamo essere sicuri che non reintrodurremo inavvertitamente questo specifico bug perché l’esecuzione del test ci avviserà immediatamente. Possiamo considerare questa piccola parte dell’applicazione bloccata in modo sicuro per sempre.
Test più completi¶
Già che ci siamo, possiamo aggiustare meglio il metodo was_published_recently()
; in effetti, potrebbe essere estremamente imbarazzante se nel tentativo di correggere un bug ne introducessimo un altro.
Aggiungi altri due metodi di test alla stessa classe, per testare il comportamento del metodo in modo più esteso:
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)
Ed adesso abbiamo tre test che confermano che Question.was_published_recently()
restuisce valori ragionevoli per le domande passate, recenti e future.
Di nuovo, polls
è una applicazione minimale ma a prescindere da quanto cresca in complessità o con quale codice interagisca, adesso abbiamo qualche garanzia che i metodi per cui abbiamo scritto test si comporteranno come ci aspettiamo.
Testare una vista¶
L’applicazione dei sondaggi agisce in modo abbastanza indiscriminato: pubblicherà ogni domanda, incluse quelle il cui campo pub_date
è futuro. Dovremmo migliorare questa cosa. Impostare una pub_date
nel futuro significa che la domanda deve essere pubblicata in quel momento ma fino ad allora deve essere invisibile.
Un test per una vista¶
Quando abbiamo corretto il bug sopra, abbiamo scritto prima il test e poi il codice per risolverlo. In effetti è stato un esempio di sviluppo basato sui test (TDD), ma non importa in quale ordine svolgiamo il lavoro.
Nel nostro primo test, ci siamo concentrati sul comportamento interno del codice. Per questo test, vogliamo verificarne il comportamento come se fosse fatto da un utente tramite un browser web.
Prima di provare a riparare qualcosa, diamo un’occhiata agli strumenti a nostra disposizione.
Il client test di Django¶
Django offre una Client
di test per simulare un utente che interagisce con il codice a livello della view. Possiamo usarla in tests.py
o persino nella shell
.
Cominciamo di nuovo con la shell
, dove dobbiamo fare un paio di cose che non saranno necessarie in tests.py
. La prima è approntare l’ambiente di test nella shell
:
$ python manage.py shell
...\> py manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()
installa un renderer di template che ci permette di esaminare alcuni attributi in più sulle responses, come response.context
che altrimenti non sarebbe disponibile. Nota che questo metodo non inizializza un database di test, ciò che segue quindi sarà lanciato sul database esistente e l’output può differire leggermente, dipendentemente dalle domande che hai già creato. Puoi ottenere risultati inaspettati se la tua TIME_ZONE
in settings.py
non è corretta. Se non ti sei ricordato di impostarla prima, controllala prima di continuare.
Poi abbiamo bisogno di importare la classe client per il test (più tardi in tests.py
useremo la classe django.test.TestCase
, che arriva con il suo client, così che questo non sarà necessario):
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
Preparato ciò, possiamo chiedere al client di fare del lavoro per noi:
>>> # 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?>]>
Migliorare la nostra vista¶
La lista di questionari mostra i questionari che non sono ancora stati pubblicati (cioè quelli la cui pub_date
è futura). Mettiamo a posto questa cosa.
Nel Tutorial 4 abbiamo introdotto una view basate su classe, basata su 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]
Dobbiamo modificare il metodo get_queryset()
e cambiarlo in modo che controlli anche la data confrontandola con timezone.now()
. Per prima cosa dobbiamo aggiungere un import:
from django.utils import timezone
e poi dobbiamo correggere il metodo get_queryset
in questo modo:
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())
restituisce un queryset contenente le Question
la cui pub_date
è minore o uguale a - cioè prima o allo stesso momento di - timezone.now
.
Testare la nostra nuova vista¶
Adesso puoi renderti orgoglioso del fatto che questo si comporti come ci si aspetta lanciando runserver
, caricando il sito nel browser, creando Questions
le cui date sono passate e future e controllando che solo quelle pubblicate appaiano nella lista. Non lo vuoi fare tutte le volte che cambi qualcosa che potrebbe aver ripercussioni su questo - quindi creiamo anche un test, basato sulla nostra sessione shell
di prima.
Aggiungere quanto segue a polls/tests.py
:
from django.urls import reverse
e creeremo una funzione scorciatoia per creare domande così come una nuiova classe di test:
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],
)
Diamo un occhiata da vicino.
La prima è una funzione scorciatoia, create_question
, che astrae alcune ripetizioni nel processo di creazione delle domande.
test_no_questions
non crea domande, ma controlla il messaggio: «Non ci sono sondaggi disponibili.» e verifica che latest_question_list
sia vuota. Nota che la classe django.test.TestCase
offre alcuni metodi di asserzioni aggiuntivi. In questi esempi, usiamo assertContains()
e assertQuerysetEqual()
.
In test_past_question
, creiamo una domanda e verifichiamo che appaia nell’elenco.
In test_future_question
, creiamo una domanda con una pub_date
nel futuro. Il database viene reimpostato per ogni metodo di test, quindi la prima domanda non c’è più e così, di nuovo, l’indice non dovrebbe mostrare nessuna domanda.
E così via. In effetti, stiamo usando i test per raccontare la storia dell’input dell’admin e la user experience sul sito, controllando ad ogni stato e per ogni cambiamento del sito, che i cambiamenti vengano pubblicati.
Testare la DetailView
¶
Quel che abbiamo funziona bene; eppure, anche se le domande future non appaiono nell”indice, gli utenti possono ancora raggiungere se conoscono o indovinano la URL giusta. Quindi dobbiamo aggiungere una restrizione simile a 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())
Dovremmo quindi aggiungere alcuni test, per verificare che una Question
la cui pub_date
sia nel passato, possa essere visualizzata e quella con pub_date
nel futuro non lo sia:
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)
Idee per altri test¶
Dovremmo aggiungere un metodo simile a get_queryset
a ResultsView
e creare una nuova classe di test per quella view. Sarà molto simile a quello che abbiamo appena creato; in effetti ci saranno un sacco di ripetizioni.
Noi potremmo migliorare la nostra applicazione in altri modi, aggiungendo test. Per esempio, è sciocco che Questions
possa essere pubblicato sul sito ma non ha Choices
. Le nostre view potrebbero controllare questo scenario, ed escludere questo tipo di Questions
. I nostri test potrebbero creare una Question
senza Choices
e quindi testare che non sia pubblicata, così come potremmo creare una Question
simile, ma con Choices
e testare che venga pubblicata
Probabilmente gli utenti amministratori loggati dovrebbero poter vedere le Questions
non pubblicate, ma non gli utenti normali. Sottolineamo: qualsiasi cosa venga aggiunta al codice per ottenere questo scopo, dovrebbe essere coperto da un test, sia che tu scriva prima il test e poi il codice per superare il test, sia che lavori sulla logica del codice e poi scrivi un test che la copra.
Ad un certo punto sarai obbligato a guardare i tuoi test e chiederti se il tuo codice soffra di eccesso di test , il che ci porta a:
Nei test, di più è meglio¶
Potrebbe sembrare che i nostri test aumentino senza controllo. A questa velocità ci sarà in breve tempo più codice nei nostri test che nella nostra applicazione, e la ripetizione è antiestetica, in comparazione con l’eleganza della brevità del resto del nostro codice.
** Non importa **. Lascia che crescano. Per la maggior parte puoi scrivere un test una volta e poi dimenticartene. Continuerà a svolgere la sua funzione mentre continui a sviluppare il programma.
A volte i test avranno bisogno di un aggiornamento. Mettiamo caso che cambiano le nostre view in modo che solo Questions
con Choices
vengano pubblicate. In questo caso, molti dei nostri test falliranno - *dicendoci esattamente quali test bisogna aggiornare, così che l’aggiornamento farà in modo da farli superare al prossimo giro.
Al peggio, dal momento che continui a sviluppare, potrai trovarti ad avere alcuni test che ora sono ridondanti. Nonostante questo, non è un problema; testare la ridondanza è una buona prassi.
Finché i tuoi test sono organizzati in modo ragionevole, non diventeranno ingestibili. Le buone regole pratiche includono avere:
- Una
TestClass
separata per ogni model o view - un metodo test separato per ogni set di condizioni che tu vuoi testare
- nomi di metodi di test che descrivono la loro funzione
Altri test¶
Questo tutorial introduce solo alcune delle basi del testing. C’è molto di più che puoi fare ed una serie di strumenti molto utili sono a tua disposizione per realizzare cose molto ingegnose.
Per esempio, mentre alcuni nostri test qui hanno coperto un po” di logica interna di un modello ed il modo in cui le view pubblicano le informazioni, puoi usare un framework «in-browser» come Selenium per controllare il modo in cui il tuo HTML viene mostrato nel browser. Questi tool inoltre ti permettono di controllare non solo il comportamento del tuo codice Django ma anche, per esempio, del tuo JavaScript. E” veramente fantastico vedere come i test lanciano il browser e ci interagiscono, come se si trattasse di un essere umano! Django include LiveServerTestCase
per facilitare l’integrazione con tool come Selenium.
Se hai un’applicazione complessa, potresti voler eseguire i test automaticamente ad ogni commit ai fini dell’integrazione continua, in modo che il controllo di qualità sia esso stesso, almeno parzialmente, automatizzato.
Un buon modo per individuare parti della tua applicazione non testate è controllare la code coverage. Questo aiuta anche a identificare codice morto o fragile. Se tu non riesci a testare un pezzo di codice, di solito significa che quel codice dovrebbe essere rimosso o rielaborato. La Coverage aiuta a identificare codice non più in uso. Vedi Integration with coverage.py per avere dettagli.
Testare in Django contiene informazioni complete sui test.
Cos’altro?¶
Per i dettagli completi sui test, vedi Testare in Django.
Quando ti senti pronto e preparato nel testare le view Django, leggi parte 6 di questo tutorial per imparare a gestire i file statici.