Écriture et lancement de tests

Ce document est partagé en deux sections principales. Premièrement, nous expliquons comment écrire des tests avec Django. Puis, nous expliquons comment les exécuter.

Écriture de tests

Les tests unitaires de Django utilisent le module unittest de la bibliothèque Python standard. Ce module définit les tests selon une approche basée sur des classes.

Voici un exemple de classe héritant de django.test.TestCase, elle-même héritant de unittest.TestCase, et qui lance chaque test dans une transaction pour garantir l’isolation :

from django.test import TestCase
from myapp.models import Animal


class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

Lorsque vous lancez vos tests, le comportement par défaut de l’utilitaire de test est de rechercher toutes les classes de cas de test (c’est-à-dire les sous-classes de unittest.TestCase) dans tous les fichiers dont le nom commence par test, puis de construire automatiquement une suite de tests et d’exécuter cette suite.

Pour plus de détails au sujet de unittest, consultez la documentation de Python.

À quel endroit les tests devraient-ils se trouver ?

Le gabarit par défaut de startapp crée un fichier tests.py dans la nouvelle application. Cela convient bien quand il n’y a que quelques tests. Mais dès que la suite de tests grandit, il devient préférable de restructurer ce fichier en un module Python afin de pouvoir séparer les tests en sous-modules tels que test_models.py, test_views.py, test_forms.py, etc. Vous êtes libre de choisir le découpage structurel qui vous convient.

Voir aussi Utilisation du lanceur de tests de Django pour tester des applications réutilisables.

Avertissement

Si des tests dépendent de l’accès à une base de données pour créer ou interroger des modèles, les classes de test doivent être des sous-classes de django.test.TestCase plutôt que de unittest.TestCase.

En utilisant unittest.TestCase, on allège les tests en évitant l’étape d’envelopper chaque test dans une transaction et de réinitialiser la base de données. Mais si les tests concernés interagissent avec la base de données, leur comportement variera en fonction de l’ordre dans lequel ils seront lancés par le lanceur de tests. Cela peut conduire certains tests unitaires à réussir lorsqu’il sont exécutés individuellement, mais à échouer lorsqu’ils font partie d’une suite.

Lancement des tests

Quand les tests ont été écrits, il s’agit de les lancer avec la commande test de l’utilitaire manage.py du projet :

$ ./manage.py test

La découverte des tests se base sur le module de unittest de découverte intégrée des tests. Par défaut, les tests sont recherchés dans tout fichier nommé test*.py dans toute l’arborescence à partir du répertoire de travail courant.

Vous pouvez sélectionner certains tests spécifiques à exécuter en indiquant des « noms de tests » à la commande ./manage.py test. Chaque nom de test peut être un chemin Python complet en syntaxe pointée vers un paquet, un module, une sous-classe de TestCase ou une méthode de test. Par exemple :

# Run all the tests in the animals.tests module
$ ./manage.py test animals.tests

# Run all the tests found within the 'animals' package
$ ./manage.py test animals

# Run just one test case class
$ ./manage.py test animals.tests.AnimalTestCase

# Run just one test method
$ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak

Il est aussi possible d’indiquer un chemin vers un répertoire pour rechercher tous les tests dans l’arborescence de ce répertoire :

$ ./manage.py test animals/

Vous pouvez indiquer un motif de nom de fichier personnalisé en utilisant l’option -p (ou --pattern), dans le cas où vos fichiers de test ne correspondent pas au motif test*.py:

$ ./manage.py test --pattern="tests_*.py"

Si vous appuyez sur Ctrl-C pendant que les tests sont en cours d’exécution, le lanceur de tests attend que le test actuellement en cours se termine puis quitte les tests de manière harmonieuse. En quittant les tests de cette façon, le lanceur affiche les détails des échecs de tests, indique combien de tests ont été lancés ainsi que le nombre d’erreurs et d’échecs rencontrés, puis il supprime la ou les bases de données de test comme il le fait d’habitude. Ainsi, Ctrl-C peut être très utile si vous oubliez de passer l’option --failfast, que vous réalisez que certains tests échouent de manière inattendue et que vous souhaitez obtenir les détails de ces échecs sans attendre que toute la suite de tests se termine.

Si vous ne voulez pas attendre que le test actuellement en cours se termine, vous pouvez appuyer une seconde fois sur Ctrl-C et le test se terminera immédiatement, mais de manière brusque. Vous ne verrez aucun détail sur les tests lancés jusqu’à ce point et toute base de données de test créée pour cette exécution ne sera pas supprimée.

Tests avec avertissements actifs

Il est conseillé de lancer les tests en activant les avertissements de Python : python -Wa manage.py test. Le drapeau -Wa indique à Python d’afficher les avertissements d’obsolescence. Django, comme toute autre bibliothèque Python, utilise ces avertissements pour notifier que certaines fonctionnalités sont sur le point de disparaître. Certains avertissements peuvent aussi signaler que certaines zones du code ne sont pas fondamentalement fausses mais qu’elles pourraient bénéficier d’une meilleure implémentation.

La base de données de test

Les tests ayant besoin d’une base de données (à savoir les tests avec des modèles) n’utilisent pas la « vraie » base de données (de production). Des bases de données distinctes et vierges sont créées tout exprès pour les tests.

Que les tests passent ou échouent, les bases de données de test sont supprimées dès que tous les tests ont été exécutés.

Vous pouvez éviter de supprimer les bases de données de test en utilisant l’option test --keepdb. Cela permet de conserver la base de données de test entre les sessions. Si la base de données n’existe pas, elle sera tout de même créée. Toute migration sera aussi appliquée afin de maintenir la base à jour.

Comme expliqué dans la section précédente, si une exécution de tests est interrompue de manière brusque, la base de données de test ne sera peut-être pas détruite. Au prochain lancement, on vous demandera si vous souhaitez réutiliser ou détruire la base de données. Utilisez l’option test --noinput pour supprimer cette question et automatiquement détruire la base de données. Cela peut se révéler utile lors du lancement des tests sur un serveur d’intégration continue où les tests peuvent être stoppés par un délai d’expiration, par exemple.

Les noms des bases de données de test par défaut sont créés en préfixant les valeurs de chaque NAME dans DATABASES avec test_ . Avec SQLite, les tests utilisent par défaut une base de données en mémoire (c’est-à-dire que la base de données est créée en mémoire sans passer par le système de fichiers). Le dictionnaire TEST dans DATABASES offre plusieurs réglages pour configurer la base de données de test. Par exemple, si vous souhaitez utiliser un autre nom de base de données, renseignez NAME dans le dictionnaire TEST de la base de données concernée dans DATABASES.

Avec PostgreSQL, USER a aussi besoin de l’accès en lecture à la base de données intégrée postgres.

Excepté la création d’une base de données séparée, le lanceur de tests utilise les mêmes réglages de base de données de votre fichier de réglages : ENGINE, USER, HOST, etc. La base de données de test est créée par l’utilisateur USER, vous devez donc vérifier que ce compte utilisateur possède les droits nécessaires pour créer une nouvelle base de données dans votre système.

Pour un contrôle plus fin sur le codage de caractères de la base de données de test, utilisez l’option CHARSET de TEST. Avec MySQL, il est aussi possible d’utiliser l’option COLLATION pour contrôler la collation utilisée par la base de données de test. Consultez la documentation des réglages pour plus de détails sur ces réglages avancés.

Si vous utilisez une base de données SQLite en mémoire, le cache partagé est activé, afin que vous puissiez écrire des tests avec la possibilité de partager la base de données entre fils d’exécutions (threads).

Accès à des données de la base de données de production lors du fonctionnement des tests ?

Si votre code essaie d’accéder à la base de données au moment de la compilation du code, cela se produira avant que la base de données ne soit configurée, avec des résultats potentiellement inattendus. Par exemple, si une requête de base de données est lancée dans du code au niveau module et qu’une base de données réelle existe, des données de production pourraient interférer avec les tests. Il est de toute manière fortement déconseillé de placer des requêtes de base de données exécutées à l’importation du code, il s’agit de réécrire le code pour que cela n’arrive pas.

Ceci s’applique aussi aux implémentations personnalisées de ready().

Ordre d’exécution des tests

Pour pouvoir garantir que tout code de TestCase commence avec une base de données propre, le lanceur de tests de Django réordonne les tests de la manière suivante :

  • Toutes les sous-classes de TestCase sont exécutées en premier.

  • Puis, tous les autres tests basés sur Django (classes de cas de tests basées sur SimpleTestCase, y compris TransactionTestCase) sont exécutés sans qu’un ordre de tri particulier ne soit garanti.

  • Puis tous les autres tests unittest.TestCase (y.c. les « doctests ») qui pourraient modifier la base de données sans la restaurer à son état de départ sont lancés.

Note

Le nouvel ordre d’exécution des tests pourrait révéler des dépendances non prévues dans l’ordre des cas de test. C’est le cas avec les « doctests » qui comptaient sur un état de base de données suite à un test TransactionTestCase donné ; ces tests doivent être mis à jour pour pouvoir fonctionner de manière indépendante.

Note

Les échecs détectés lors du chargement des tests sont positionnés avant tous les autres pour un retour d’information plus rapide. Cela inclut les choses telles que les modules de test qui n’ont pas pu être trouvés ou qui n’ont pas pu être chargés en raison d’erreurs de syntaxe.

Vous pouvez inverser ou rendre aléatoire l’ordre d’exécution à l’intérieur des groupes en passant les options test --reverse et test --shuffle. Ceci peut aider à s’assurer que les tests sont indépendants les uns des autres.

Émulation d’annulation de transaction (« rollback »)

Les éventuelles données initiales chargées dans les migrations ne sont disponibles que dans les tests TestCase, mais pas dans les tests TransactionTestCase et de plus uniquement avec les moteurs qui prennent en charge les transactions (l’exception la plus notable étant MyISAM). C’et aussi vrai pour les tests qui se basent sur TransactionTestCase tels que LiveServerTestCase et StaticLiveServerTestCase.

Django peut recharger ces données à votre place pour chaque cas de test si vous définissez l’option serialized_rollback à True dans le corps de TestCase ou de TransactionTestCase, mais vous devez savoir que cela ralentira la suite de tests concernée d’environ 3 fois.

Les applications tierces ou celles qui se basent sur le moteur MyISAM ont besoin de définir cet attribut. Cependant, il est conseillé de manière générale de développer ses projets en s’appuyant sur une base de données transactionnelle et de tester avec la classe de tests TestCase, et dans ce cas, cet attribut n’est pas nécessaire.

La sérialisation initiale est généralement très rapide, mais si vous souhaitez exclure certaines applications de ce processus (et accélérer légèrement l’exécution des tests), vous pouvez ajouter ces applications au réglage TEST_NON_SERIALIZED_APPS.

Pour éviter que des données sérialisées soient chargées deux fois, on peut définir serialized_rollback=True pour désactiver le signal post_migrate lors de la réinitialisation de la base de données de test.

Autres conditions de test

Tous les tests de Django sont exécutés avec DEBUG=False, quel que soit la valeur du réglage DEBUG dans votre fichier de réglages. Cela permet de garantir que le résultat observé du code correspond à ce qui sera effectivement produit en production.

Les caches ne sont pas effacés après chaque test et le lancement de manage.py test fooapp peut insérer des données de test dans le cache d’un système en ligne si vous lancez les tests en production car au contraire de ce qui est fait avec les bases de données, les tests n’utilisent pas un « cache de test ». Ce comportement pourrait changer à l’avenir.

Compréhension de l’affichage des tests

Lorsque vous lancez des tests, un certain nombre de messages apparaissent alors que le lanceur de test se prépare. Vous pouvez contrôler le niveau de détail de ces messages avec l’option verbosity en ligne de commande :

Creating test database...
Creating table myapp_animal
Creating table myapp_mineral

Le lanceur de tests vous informe qu’il crée une base de données de test, comme indiqué dans la section précédente.

Après la création de la base de données de test, Django se met à exécuter les tests proprement dits. Si tout se passe bien, vous verrez quelque chose comme ceci :

----------------------------------------------------------------------
Ran 22 tests in 0.221s

OK

Cependant, si des tests ont échoué, vous verrez des détails complets au sujet des tests ayant échoué :

======================================================================
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
    self.assertIs(future_poll.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

Il n’est pas dans l’optique de ce document d’expliquer en détails cet affichage d’erreur, mais c’est relativement intuitif. Vous pouvez consulter la documentation Python de la bibliothèque unittest pour plus de détails.

Notez que le code de retour du script de lancement des tests est 1 quel que soit le nombre de tests en échec (que l’échec soit causé par une erreur, une assertion incorrecte ou un succès inattendu). Si tous les tests passent, le code de retour est 0. Cette fonctionnalité est utile si vous lancez le script de lancement des tests à partir d’un script shell et que vous avez besoin de tester le résultat (succès ou échec) à ce niveau.

Accélération des tests

Lancement des tests en parallèle

Tant que vos tests sont proprement isolés, il est possible de les exécuter en parallèle pour gagner du temps sur du matériel à plusieurs cœurs. Voir test --parallel.

Hachage de mots de passe

Le hachage par défaut du mot de passe est délibérément assez lent. Si vos tests font l’authentification de beaucoup d’utilisateurs, il est conseillé de créer un fichier de réglages propre aux tests et de définir le réglage PASSWORD_HASHERS à un algorithme de hachage plus rapide :

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.MD5PasswordHasher",
]

N’oubliez pas d’inclure aussi dans PASSWORD_HASHERS tout algorithme de hachage utilisé dans les instantanés, le cas échéant.

Préserver la base de données de test

L’option test --keepdb préserve la base de données de test entre plusieurs exécutions. Cela ignore les actions de création et de destruction et peut grandement décroître le temps d’exécution des tests.

Éviter les accès au disque pour les fichiers médias

La classe InMemoryStorage est une manière pratique d’éviter les accès au disque pour les fichiers médias. Toutes les données sont conservées en mémoire, puis abandonnées à la fin de l’exécution des tests.

Back to Top