Tests dans Django (Partie 1) – Meilleures pratiques et exemples
Les tests sont vitaux. Sans tester correctement votre code, vous ne saurez jamais si le code fonctionne comme il se doit, maintenant ou à l'avenir lorsque la base de code changera. D’innombrables heures peuvent être perdues à résoudre des problèmes causés par des modifications apportées à la base de code. Pire encore, vous ne savez peut-être même pas qu'il y a des problèmes jusqu'à ce que vos utilisateurs finaux s'en plaignent, ce qui n'est évidemment pas ainsi que vous souhaitez en savoir plus sur les ruptures de code.
La mise en place de tests aidera à garantir que si une fonction spécifique tombe en panne, vous en serez informé. Les tests facilitent également grandement le débogage des interruptions de code, ce qui permet d'économiser du temps et de l'argent.
J'ai littéralement perdu des concerts dans le passé en ne testant pas correctement les nouvelles fonctionnalités par rapport à l'ancienne base de code. Ne laisses pas cela t'arriver. Prenez les tests au sérieux. Vous aurez plus confiance en votre code et votre employeur aura plus confiance en vous. Il s’agit essentiellement d’une police d’assurance.
Les tests aident à structurer un bon code, à trouver des bogues et à rédiger de la documentation.
Dans cet article, nous examinerons d’abord une brève introduction qui inclut les meilleures pratiques avant d’examiner quelques exemples.
Introduction aux tests dans Django
Types d'examens
Les tests unitaires et d'intégration sont les deux principaux types de tests :
- Les Tests unitaires sont des tests isolés qui testent une fonction spécifique.
- Les tests d'intégration, quant à eux, sont des tests plus vastes qui se concentrent sur le comportement des utilisateurs et testent des applications entières. En d’autres termes, les tests d’intégration combinent différentes fonctionnalités de code pour garantir qu’elles se comportent correctement.
Concentrez-vous sur les tests unitaires. Écrivez BEAUCOUP de ceux-ci. Ces tests sont beaucoup plus faciles à écrire et à déboguer que les tests d'intégration, et plus vous en avez, moins vous aurez besoin de tests d'intégration. Les tests unitaires doivent être rapides. Nous examinerons quelques techniques pour accélérer les tests.
Cela dit, les tests d'intégration sont parfois encore nécessaires même si vous disposez d'une couverture en tests unitaires, car les tests d'intégration peuvent aider à détecter les régressions de code.
En général, les tests aboutissent soit à un succès (résultats attendus), à un échec (résultats inattendus) ou à une erreur. Vous devez non seulement tester les résultats attendus, mais également la façon dont votre code gère les résultats inattendus.
Les meilleures pratiques
- S'il peut se briser, il faut le tester. Cela inclut les modèles, les vues, les formulaires, les modèles, les validateurs, etc.
- Chaque test ne doit généralement tester qu’une seule fonction.
- Rester simple. Vous ne voulez pas avoir à écrire des tests par-dessus d’autres tests.
- Exécutez des tests chaque fois que le code est PULL ou PUSH depuis le référentiel et dans l'environnement de test avant de le PUSH vers la production.
Lors de la mise à niveau vers une version plus récente de Django :
- mettre à niveau localement,
- exécutez votre suite de tests,
- corriger les bugs,
- PUSH vers le dépôt et la mise en scène, puis
- testez à nouveau en préparation avant d'envoyer le code.
Structure
Structurez vos tests pour les adapter à votre projet. J'ai tendance à préférer placer tous les tests de chaque application dans le fichier tests.py et regrouper les tests en fonction de ce que je teste - par exemple, modèles, vues, formulaires, etc.
Vous pouvez également contourner (supprimer) le fichier tests.py et structurer vos tests de cette manière dans chaque application :
└── app_name
└── tests
├── __init__.py
├── test_forms.py
├── test_models.py
└── test_views.py
Enfin, vous pouvez créer un dossier de test distinct, qui reflète la structure entière du projet, en plaçant un fichier tests.py dans chaque dossier d'application.
Les projets plus importants devraient utiliser l'une de ces dernières structures. Si vous savez que votre petit projet finira par évoluer vers quelque chose de beaucoup plus grand, il est préférable d’utiliser également l’une de ces deux dernières structures. Je privilégie la première et la troisième structures, car je trouve plus facile de concevoir des tests pour chaque application lorsqu'elles sont toutes visibles dans un seul script.
Forfaits tiers
Utilisez les packages et bibliothèques suivants pour vous aider à écrire et à exécuter votre suite de tests :
- django-webtest : facilite grandement l'écriture de tests fonctionnels et d'assertions qui correspondent à l'expérience de l'utilisateur final. Associez ces tests aux tests Selenium pour une couverture complète des modèles et des vues.
- couverture : est utilisé pour mesurer l'efficacité des tests, en affichant le pourcentage de votre base de code couverte par les tests. Si vous commencez tout juste à mettre en place des tests unitaires, la couverture peut vous aider à proposer des suggestions sur ce qui doit être testé. La couverture peut également être utilisée pour transformer les tests en jeu : j'essaie d'augmenter le pourcentage de code couvert par les tests chaque jour, par exemple.
- django-discover-runner : aide à localiser les tests si vous les organisez d'une manière différente (par exemple, en dehors de tests.py). Ainsi, si vous organisez vos tests dans des dossiers séparés, comme dans l'exemple ci-dessus, vous pouvez utiliser discover-runner pour localiser les tests.
- factory_boy, model_mommy et mock : tous sont utilisés à la place des appareils ou de l'ORM pour remplir les données nécessaires aux tests. Les appareils et l'ORM peuvent être lents et doivent être mis à jour chaque fois que votre modèle change.
Exemples
Dans cet exemple basique, nous allons tester :
- des modèles,
- vues,
- des formulaires, et
- l'API.
Téléchargez le dépôt Github ici pour suivre.
Installation
Installez la couverture et ajoutez-la à votre INSTALLED_APPS :
$ pip install coverage==3.6
Couverture d'exécution :
$ coverage run manage.py test whatever -v 2
Utilisez le niveau de verbosité 2,
-v 2
, pour plus de détails. Vous pouvez également tester l'intégralité de votre projet Django en une seule fois avec cette commande :coverage run manage.py test -v 2
.
Créez votre rapport pour voir par où commencer les tests :
$ coverage html
Ouvrez django15/htmlcov/index.html pour voir les résultats de votre rapport. Faites défiler vers le bas du rapport. Vous pouvez ignorer toutes les lignes du dossier virtualenv. Ne testez jamais quoi que ce soit qui soit une fonction ou une bibliothèque Python intégrée, car celles-ci sont déjà testées. Vous pouvez déplacer votre virtualenv hors du dossier pour rendre le rapport plus propre après son exécution.
Commençons par tester les modèles.
Modèles de test
Dans le rapport de couverture, cliquez sur le lien « quels que soient les modèles ». Vous devriez voir cet écran :
Essentiellement, ce rapport indique que nous devrions tester le titre d’une entrée. Simple.
Ouvrez tests.py et ajoutez le code suivant :
from django.test import TestCase
from whatever.models import Whatever
from django.utils import timezone
from django.core.urlresolvers import reverse
from whatever.forms import WhateverForm
# models test
class WhateverTest(TestCase):
def create_whatever(self, title="only a test", body="yes, this is only a test"):
return Whatever.objects.create(title=title, body=body, created_at=timezone.now())
def test_whatever_creation(self):
w = self.create_whatever()
self.assertTrue(isinstance(w, Whatever))
self.assertEqual(w.__unicode__(), w.title)
Que se passe-t-il ici ? Nous avons essentiellement créé un objet Whatever
et testé si le titre créé correspondait au titre attendu - ce qui a été fait.
Remarque : assurez-vous que les noms de vos fonctions commencent par
test_
, ce qui est non seulement une convention courante mais également pour que django-discover-runner puisse localiser le test. Écrivez également des tests pour toutes les méthodes que vous ajoutez à votre modèle.
Réexécution de la couverture :
$ coverage run manage.py test whatever -v 2
Vous devriez voir les résultats suivants, indiquant que le test a réussi :
test_whatever_creation (whatever.tests.WhateverTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
Ensuite, si vous regardez à nouveau le rapport de couverture, les modèles devraient maintenant être à 100 %.
Test des vues
Tester les vues peut parfois être difficile. J'utilise généralement des tests unitaires pour vérifier les codes d'état ainsi que Selenium Webdriver pour tester AJAX, Javascript, etc.
Ajoutez le code suivant à la classe WhichTest dans tests.py :
# views (uses reverse)
def test_whatever_list_view(self):
w = self.create_whatever()
url = reverse("whatever.views.whatever")
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(w.title, resp.content)
Ici, nous récupérons l'URL du client, stockons les résultats dans la variable resp
puis testons nos assertions. Tout d’abord, nous testons si le code de réponse est 200, puis nous testons la réponse réelle. Tu devrais obtenir le résultats suivants :
test_whatever_creation (whatever.tests.WhateverTest) ... ok
test_whatever_list_view (whatever.tests.WhateverTest) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.052s
OK
Exécutez à nouveau votre rapport. Vous devriez maintenant voir un lien pour « whatever/views », affichant les résultats suivants :
Vous pouvez également écrire des tests pour vous assurer que quelque chose échoue. Par exemple, si un utilisateur doit être connecté pour créer un nouvel objet, le test réussira s'il échoue réellement à créer l'objet.
Regardons un test rapide du sélénium :
# views (uses selenium)
import unittest
from selenium import webdriver
class TestSignup(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
def test_signup_fire(self):
self.driver.get("http://localhost:8000/add/")
self.driver.find_element_by_id('id_title').send_keys("test title")
self.driver.find_element_by_id('id_body').send_keys("test body")
self.driver.find_element_by_id('submit').click()
self.assertIn("http://localhost:8000/", self.driver.current_url)
def tearDown(self):
self.driver.quit
if __name__ == '__main__':
unittest.main()
Installez le sélénium :
$ pip install selenium==2.33.0
Exécutez les tests. Firefox devrait se charger (si vous l'avez installé) et exécuter le test. Nous affirmons ensuite que la bonne page est chargée lors de la soumission. Vous pouvez également vérifier que le nouvel objet a été ajouté à la base de données.
Formulaires de test
Ajoutez les méthodes suivantes :
def test_valid_form(self):
w = Whatever.objects.create(title='Foo', body='Bar')
data = {'title': w.title, 'body': w.body,}
form = WhateverForm(data=data)
self.assertTrue(form.is_valid())
def test_invalid_form(self):
w = Whatever.objects.create(title='Foo', body='')
data = {'title': w.title, 'body': w.body,}
form = WhateverForm(data=data)
self.assertFalse(form.is_valid())
Remarquez comment nous générons les données du formulaire à partir de JSON. C'est un incontournable.
Vous devriez maintenant avoir 5 tests de réussite :
test_signup_fire (whatever.tests.TestSignup) ... ok
test_invalid_form (whatever.tests.WhateverTest) ... ok
test_valid_form (whatever.tests.WhateverTest) ... ok
test_whatever_creation (whatever.tests.WhateverTest) ... ok
test_whatever_list_view (whatever.tests.WhateverTest) ... ok
----------------------------------------------------------------------
Ran 5 tests in 12.753s
OK
Vous pouvez également écrire des tests qui vérifient si un certain message d'erreur est affiché en fonction des validateurs dans le formulaire lui-même.
Tester l'API
Tout d'abord, vous pouvez accéder à l'API à partir de cette URL : http://localhost:8000/api/whatever/?format=json. Il s’agit d’une configuration simple, donc les tests seront également assez simples.
Installez lxml et XML désamorcé :
$ pip install lxml==3.2.3
$ pip install defusedxml==0.4.1
Ajoutez les cas de test suivants :
from tastypie.test import ResourceTestCase
class EntryResourceTest(ResourceTestCase):
def test_get_api_json(self):
resp = self.api_client.get('/api/whatever/', format='json')
self.assertValidJSONResponse(resp)
def test_get_api_xml(self):
resp = self.api_client.get('/api/whatever/', format='xml')
self.assertValidXMLResponse(resp)
Nous affirmons simplement que nous obtenons une réponse dans chaque cas.
La prochaine fois
Dans le prochain didacticiel, nous examinerons un exemple plus compliqué et utiliserons model_mommy pour générer des données de test. Encore une fois, vous pouvez récupérer le code du dépôt.
Vous avez quelque chose à ajouter ? Laissez un commentaire ci-dessous.