• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Testez votre application

Log in or subscribe for free to enjoy all this course has to offer!

L'application est prête à être mise en ligne mais il subsiste un élément dont je ne vous ai pas encore parlé : les tests.

Jusqu'à maintenant, vous avez testé votre projet à la main. Cela signifie que l'ajout d'une nouvelle fonctionnalité vous oblige à vérifier que les anciennes fonctionnent encore. C'est plutôt pénible et très rébarbatif.

La bonne nouvelle est que nous sommes des développeurs ! Nous pouvons tout faire ! Si certains accomplissent l'exploit de coder un robot aussi savant que les médecins, nous pouvons bien automatiser quelques tests !

Les tests automatisés ne me disent absolument rien. Aurais-tu une ressource à me conseiller ?

Bien sûr ! Il se trouve que j'ai réalisé un autre cours spécialement sur le sujet : Testez votre projet avec Python. Vous y apprendrez à tester un projet basique en Python. Vous verrez également comment écrire des tests en suivant une méthodologie de projet dans laquelle le développement est piloté par les tests. Je vous conseille vivement de le suivre avant de lire ce chapitre.

Django et les tests

Il est très simple d'ajouter des tests dans Django. Le framework inclut une classe TestCase qui hérite de Unittest.

Unittest ? Késako ?

Unittest est le module de tests par défaut. Il est intégré dans la librairie standard de Python, ce qui signifie que vous n'avez pas à l'installer ! Youpi !

Commencez par vous questionner sur les éléments que vous souhaitez tester. Les modèles ? Les vues ? Quels sont les tests que vous réalisez manuellement et que vous aimeriez automatiser ?

Etant donné que nous n'avons pas de méthode additionnelle dans les modèles, je ne trouve pas cela pertinent de les tester tout de suite. Concentrons-nous sur les vues et déterminons ce que nous pouvons tester.

Une vue est une méthode Python, il est donc très aisé de vérifier que les éléments renvoyés correspondent à ce qui est attendu.

Faites la liste de votre côté puis regardez la mienne :

  • page d'accueil : elle doit renvoyer un code de statut 200

  • page de détail : elle doit renvoyer un code de statut 200 si l'album demandé existe.

  • page de détail : elle doit renvoyer un code de statut 404 si l'album demandé n'existe pas.

  • page de réservation : après une requête POST contenant des données valides, une nouvelle réservation doit être réalisée.

  • page de réservation : après une requête POST contenant des données valides, une réservation doit appartenir à un contact.

  • page de réservation : après une requête POST contenant des données valides, une réservation doit appartenir à un album.

  • page de réservation : après une requête POST contenant des données valides, un album ne doit pas être disponible si la réservation est finalisée.

Ouvrez le document tests.py, généré par défaut, et ajoutez-y les lignes suivantes :

tests.py

from django.test import TestCase
# Index page
# test that index page returns a 200
# Detail Page
# test that detail page returns a 200 if the item exists
# test that detail page returns a 404 if the item does not exist
# Booking Page
# test that a new booking is made
# test that a booking belongs to a contact
# test that a booking belongs to an album
# test that an album is not available after a booking is made

Test du code de statut renvoyé par une vue

Django contient la classe TestCase qui hérite notamment de la classe du même nom de Unittest. Pour rappel, un test est une méthode dont le nom commence par test et qui ne vérifie qu'un seul élément par test.

Voici un exemple :

from django.test import TestCase
class IndexPageTestCase(TestCase):
def test_index_page(self):
self.assertEqual('a', 'a')

Evidemment, nous allons le modifier ! Prenez le temps de bien le lire pour en comprendre la syntaxe.

La classe TestCase inclut un navigateur web très simpliste : il est capable de recevoir vos requêtes et de vous renvoyer une réponse, mais vous ne pouvez pas interagir avec lui via une interface graphique. Autrement dit, vous n'entrez pas l'adresse souhaitée dans la barre de navigation mais vous la passez dans une méthode !

Par exemple, imaginons que vous voulez naviguer sur la page d'accueil de votre application. Vous communiquerez au programme la demande suivante : "Va sur la page d'accueil et dis-moi quel est le contenu de la page". Le navigateur allégé va sur la page et vous renvoie son contenu HTML : "<!DOCTYPE html><html lang="fr">...". Vous ne voyez néanmoins pas la page s'afficher sous vos yeux.

Ce navigateur est accessible via l'objet client dans TestCase. Il contient plusieurs méthodes dont chaque nom reflète la méthode HTTP désirée. Par exemple, la méthode get("google.com") utilise la méthode HTTP GET pour accéder à l'URL "google.com".

Mettez à jour la méthode test_index_page pour naviguer sur la page d'accueil et vérifier le code de statut :

tests.py

from django.core.urlresolvers import reverse
class IndexPageTestCase(TestCase):
def test_index_page(self):
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)

Pour lancer les tests, tapez la commande suivante : manage.py test.

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_index_page (store.tests.IndexPageTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/celinems/sites/oc/exercices_exemples/decouvrez_django/disquaire_project/store/tests.py", line 13, in test_index_page
response = self.client.get(reverse('index'))
File "/Users/celinems/sites/oc/exercices_exemples/decouvrez_django/env/lib/python3.6/site-packages/django/urls/base.py", line 91, in reverse
return force_text(iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs)))
File "/Users/celinems/sites/oc/exercices_exemples/decouvrez_django/env/lib/python3.6/site-packages/django/urls/resolvers.py", line 497, in _reverse_with_prefix
raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'index' not found. 'index' is not a valid view function or pattern name.
----------------------------------------------------------------------
Ran 1 test in 0.012s
FAILED (errors=1)
Destroying test database for alias 'default'...

Le test échoue ! Pourquoi ? Car la page d'accueil n'a pas de nom ! Donnez-lui en un immédiatement :

disquaire_project/urls.py

urlpatterns = [
url(r'^$', views.index, name="index"),
# ...
]

Puis relancez le test.

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.207s
OK
Destroying test database for alias 'default'...

Bravo ! Vous venez de résoudre un bug que vous n'aviez pas encore identifié et d'écrire le test associé.

Tests utilisant des objets dans la base de données

Le test suivant vérifie que la page detail renvoie un code de statut 200 si l'album est bien trouvé. Cela suppose donc deux éléments :

  • une base de données existe dans l'environnement de test,

  • la base contient l'album recherché.

Là encore, Django nous est d'un grand secours. Nul besoin de créer ou de configurer une base de données dans un environnement de test : Django en génère une nouvelle au lancement du programme et la supprime à la fin. D'ailleurs, c'est écrit dans la console !

Cela vous garantit que la base de test est dédiée à cet usage et qu'elle n'entrera donc jamais en conflit avec d'autres bases (celle de développement, par exemple).

Ajout de données dans la base de données

Ajouter des données dans la base est aussi simple que dans la console Django ou dans les vues. Importez simplement le module models et utilisez l'ORM de la même manière que vous l'avez fait jusqu'à maintenant.

tests.py

from .models import Album, Artist, Contact, Booking
class DetailPageTestCase(TestCase):
# test that detail page returns a 200 if the item exists.
def test_detail_page_returns_200(self):
impossible = Album.objects.create(title="Transmission Impossible")
album_id = Album.objects.get(title='Transmission Impossible').id
response = self.client.get(reverse('store:detail', args=(album_id,)))
self.assertEqual(response.status_code, 200)
# test that detail page returns a 404 if the item does not exist.

Lancez les tests :

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.065s
OK
Destroying test database for alias 'default'...

Le programme fonctionne !

Créer les données avant chaque test

Le test suivant vérifie qu'un code de statut 404 est renvoyé si un album n'a pas été trouvé dans la page de détails.

Il est très semblable au précédent :

class DetailPageTestCase(TestCase):
def test_detail_page_returns_200(self):
# ...
# test that detail page returns a 404 if the item does not exist
def test_detail_page_returns_404(self):
impossible = Album.objects.create(title="Transmission Impossible")
album_id = Album.objects.get(title='Transmission Impossible').id + 1
response = self.client.get(reverse('store:detail', args=(album_id,)))
self.assertEqual(response.status_code, 404)

Si vous lancez de nouveau leur exécution, ils fonctionneront. Néanmoins, vous pouvez aller un peu plus loin et utiliser la méthode setUp. Elle est exécutée avant chaque test unitaire. Vous pouvez donc y indiquer les objets à créer :

class DetailPageTestCase(TestCase):
# ran before each test.
def setUp(self):
impossible = Album.objects.create(title="Transmission Impossible")
self.album = Album.objects.get(title='Transmission Impossible')
# test that detail page returns a 200 if the item exists
def test_detail_page_returns_200(self):
album_id = self.album.id
response = self.client.get(reverse('store:detail', args=(album_id,)))
self.assertEqual(response.status_code, 200)
# test that detail page returns a 404 if the item does not exist
def test_detail_page_returns_404(self):
album_id = self.album.id + 1
response = self.client.get(reverse('store:detail', args=(album_id,)))
self.assertEqual(response.status_code, 404)

Test d'une requête POST

Les derniers tests concernent les réservations. Le premier vérifie qu'une réservation est bien ajoutée en base si un nom et un email valides sont présents dans le formulaire.

Quel est le moyen le plus efficace de tester ce scénario ? Vous le savez désormais, lorsque l'utilisateur soumet son formulaire, il réalise une requête POST contenant les valeurs du formulaire. Vous pouvez donc écrire un test utilisant la méthode POST sur la vue detail et en vérifier le retour. Simple !

La méthode post permet exactement cela. Elle prend également en paramètre optionnel des arguments et un dictionnaire contenant les éléments du formulaire.

client.post('/', {'name': 'Freddie', 'email': 'fred@queen.forever'})
class BookingPageTestCase(TestCase):
# test that a new booking is made
def test_new_booking_is_registered(self):
response = self.client.post(reverse('store:detail', args=(album_id,)), {
'name': 'freddie',
'email': 'fred@queen.forever'
})

Puis ajoutez les données utiles dans le setUp :

  • un album à réserver

  • un artiste (si vous en avez envie pour le scénario mais ce n'est pas vraiment nécessaire)

  • un contact

class BookingPageTestCase(TestCase):
def setUp(self):
Contact.objects.create(name="Freddie", email="fred@queen.forever")
impossible = Album.objects.create(title="Transmission Impossible")
journey = Artist.objects.create(name="Journey")
impossible.artists.add(journey)
self.album = Album.objects.get(title='Transmission Impossible')
self.contact = Contact.objects.get(name='Freddie')

Vous pouvez à présent utiliser ces données dans la méthode test_new_booking_is_registered :

class BookingPageTestCase(TestCase):
# ...
# test that a new booking is made
def test_new_booking_is_registered(self):
album_id = self.album.id
name = self.contact.name
email = self.contact.email
response = self.client.post(reverse('store:detail', args=(album_id,)), {
'name': name,
'email': email
})

A présent, demandez-vous comment vérifier que la réservation a bien été enregistrée. Vous pouvez choisir de la retrouver dans la base de données. Une solution, que je trouve plus simple, consiste à vérifier que le nombre de réservations a augmenté d'un point après avoir envoyé la demande.

class BookingPageTestCase(TestCase):
# ...
# test that a new booking is made
def test_new_booking_is_registered(self):
old_bookings = Booking.objects.count() # count bookings before a request
album_id = self.album.id
name = self.contact.name
email = self.contact.email
response = self.client.post(reverse('store:detail', args=(album_id,)), {
'name': name,
'email': email
})
new_bookings = Booking.objects.count() # count bookings after
self.assertEqual(new_bookings, old_bookings + 1) # make sure 1 booking was added

Continuez les autres tests tout seul puis regardez ma proposition sur le dépôt de ce chapitre.

Code de ce chapitre

Retrouvez l'intégralité du code sur ce dépôt GitHub.

Example of certificate of achievement
Example of certificate of achievement