Thématiques de tests avancées

La fabrique de requêtes

class RequestFactory[source]

La classe RequestFactory partage la même API que le client de test. Cependant, au lieu de se comporter comme un navigateur, RequestFactory offre une manière de générer une instance de requête pouvant être utilisée comme premier paramètre de n’importe quelle vue. Cela permet de tester une fonction de vue de la même façon que vous testeriez toute autre fonction, comme une boîte noire, avec des données connues en entrée et en testant un résultat spécifique.

L’API de RequestFactory est un tout petit peu plus restreinte que celle du client de test :

  • Elle ne donne accès qu’aux méthodes HTTP get(), post(), put(), delete(), head(), options() et trace().

  • Toutes ces méthodes acceptent les mêmes paramètres, à l’exception de follows. Comme il ne s’agit que d’une fabrique produisant des requêtes, le traitement de la réponse est de votre responsabilité.

  • Elle ne gère pas les intergiciels. Les attributs de session et d’authentification doivent être fournis par le test lui-même, si nécessaire, pour que la vue fonctionne correctement.

Exemple

L’exemple suivant est un test unitaire simple utilisant la fabrique de requêtes :

from django.contrib.auth.models import AnonymousUser, User
from django.test import TestCase, RequestFactory

from .views import MyView, my_view

class SimpleTest(TestCase):
    def setUp(self):
        # Every test needs access to the request factory.
        self.factory = RequestFactory()
        self.user = User.objects.create_user(
            username='jacob', email='jacob@…', password='top_secret')

    def test_details(self):
        # Create an instance of a GET request.
        request = self.factory.get('/customer/details')

        # Recall that middleware are not supported. You can simulate a
        # logged-in user by setting request.user manually.
        request.user = self.user

        # Or you can simulate an anonymous user by setting request.user to
        # an AnonymousUser instance.
        request.user = AnonymousUser()

        # Test my_view() as if it were deployed at /customer/details
        response = my_view(request)
        # Use this syntax for class-based views.
        response = MyView.as_view()(request)
        self.assertEqual(response.status_code, 200)

Tests avec plusieurs base de données

Test des configurations primaire/réplique

Si vous testez une configuration avec plusieurs bases de données impliquant une réplication primaire/réplique (désignée comme maître/esclave par certaines bases de données), cette stratégie de création de bases de données de test pose un problème. Lorsque les bases de données de test sont créées, la réplication n’a pas lieu et par conséquent, les données créées sur la base primaire ne seront pas visibles sur les répliques.

Pour contourner ce problème, Django permet de définir une base de données comme un miroir de test. Considérez cet exemple (simplifié) de configuration de bases de données :

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject',
        'HOST': 'dbprimary',
         # ... plus some other settings
    },
    'replica': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject',
        'HOST': 'dbreplica',
        'TEST': {
            'MIRROR': 'default',
        },
        # ... plus some other settings
    }
}

Dans cette configuration figurent deux serveurs de base de données : dbprimary, défini par l’alias de base de données default et dbreplica, défini par l’alias replica. Comme l’on peut s’y attendre, dbreplica a été configuré par l’administrateur de base de données comme réplique en lecture de dbprimary. En temps normal, toute écriture sur default apparaît aussi sur replica.

Si Django avait créé deux bases de données de test indépendantes, cela casserait d’éventuels tests s’attendant à de la réplication. Cependant, la base de données replica a été configurée comme miroir de test (via le réglage de test MIRROR), indiquant par là que lors des tests, replica doit être traité comme un miroir de default.

Au moment de la création de l’environnement de test, aucune base ne sera créée pour replica. Par contre, la connexion vers replica sera redirigée pour pointer vers default. Par conséquent, les écritures sur default apparaîtront aussi sur replica, mais parce qu’il s’agit en réalité de la même base de données, car aucune réplication de données ne sera mise en place entre les deux bases de données.

Ordre de création des bases de données de test

Par défaut, Django part du principe que toutes les bases de données dépendent de la base de données default et va par conséquent toujours créer celle-ci en premier. Cependant, pour toute autre base de données de votre configuration de test, il n’y a aucune garantie sur l’ordre de création des bases de données.

Si votre configuration de base de données nécessite un ordre de création bien défini, vous pouvez indiquer les dépendances entre bases de données dans le réglage de test DEPENDENCIES. Considérez cet exemple (simplifié) de configuration de bases de données :

DATABASES = {
    'default': {
        # ... db settings
        'TEST': {
            'DEPENDENCIES': ['diamonds'],
        },
    },
    'diamonds': {
         ... db settings
        'TEST': {
            'DEPENDENCIES': [],
        },
    },
    'clubs': {
        # ... db settings
        'TEST': {
            'DEPENDENCIES': ['diamonds'],
        },
    },
    'spades': {
        # ... db settings
        'TEST': {
            'DEPENDENCIES': ['diamonds', 'hearts'],
        },
    },
    'hearts': {
        # ... db settings
        'TEST': {
            'DEPENDENCIES': ['diamonds', 'clubs'],
        },
    }
}

Avec cette configuration, la base de données diamonds sera créée en premier car c’est le seul alias de base de données sans dépendances. Les alias default et clubs seront ensuite créés (bien que l’ordre de création de ceux-ci ne soit pas défini), puis hearts et enfin, spades.

Si la définition de DEPENDENCIES comporte des dépendances circulaires, une exception ImproperlyConfigured sera générée.

Fonctionnalités avancées de TransactionTestCase

TransactionTestCase.available_apps

Avertissement

Cet attribut est une API privée. Il peut être modifié ou supprimé dans une version future sans période d’obsolescence, par exemple pour s’adapter à des modifications du processus de chargement des applications.

Il est utilisé pour optimiser la suite de tests de Django lui-même qui contient des centaines de modèles mais sans relations entre les modèles des différentes applications de test.

Par défaut, available_apps vaut None. Après chaque test, Django appelle flush pour réinitialiser l’état de la base de données. Toutes les tables sont ainsi vidées et le signal post_migrate est émis, ce qui recrée un type de contenu et trois permissions pour chaque modèle. Cette opération s’alourdit proportionnellement au nombre de modèles.

En définissant available_apps à une liste d’applications, Django se comporte comme si seuls les modèles de ces applications étaient disponibles. Le comportement de TransactionTestCase est modifié comme suit :

  • post_migrate est émis avant chaque test pour créer les types de contenu et les permissions de chaque modèle des applications disponibles, dans le cas où ils manqueraient.

  • Après chaque test, Django ne vide que les tables correspondants aux modèles des applications disponibles. Cependant, au niveau de la base de données, il est possible que les troncatures s’appliquent en cascade à des modèles liés dans des applications non disponibles. De plus, post_migrate n’est pas émis ; il sera émis par le prochain test TransactionTestCase, après que l’ensemble d’applications adéquat a été sélectionné.

Comme la base de données n’est pas complètement vidée, si un test crée des instances de modèles non inclus dans available_apps, ces instances vont rester et pourraient être la cause d’échecs de tests non liés. Restez prudent avec les tests employant des sessions ; le moteur de sessions par défaut les stocke dans la base de données.

Comme post_migrate n’est pas émis après la réinitialisation de la base de données, son état après un TransactionTestCase n’est pas le même qu’après un TestCase: les lignes créées par les récepteurs de post_migrate sont manquantes. Mais sachant l’ordre dans lequel les tests sont exécutés, ce n’est pas un problème, dans la mesure où soit tous les tests TransactionTestCase d’une suite de tests déclarent available_apps, soit aucun.

available_apps est obligatoire dans la propre suite de tests de Django.

TransactionTestCase.reset_sequences

En définissant reset_sequences = True d’une classe TransactionTestCase, vous vous assurez que les séquences sont toujours réinitialisées avant le lancement du test :

class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
    reset_sequences = True

    def test_animal_pk(self):
        lion = Animal.objects.create(name="lion", sound="roar")
        # lion.pk is guaranteed to always be 1
        self.assertEqual(lion.pk, 1)

À l’exception des cas où vous testez explicitement les numéros de séquence de clés primaires, il est recommandé de ne pas tester des valeurs de clés primaires figées.

L’utilisation de reset_sequences = True ralentit les tests, car la réinitialisation des clés primaires est une opération de base de données relativement coûteuse.

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

Si vous écrivez une application réutilisable, il peut être intéressant d’utiliser le lanceur de tests de Django pour lancer votre propre suite de tests et bénéficier ainsi de l’infrastructure de tests de Django.

Une pratique courante est de placer un répertoire tests en parallèle de votre code d’application, avec la structure suivante :

runtests.py
polls/
    __init__.py
    models.py
    ...
tests/
    __init__.py
    models.py
    test_settings.py
    tests.py

Examinons ce qui se trouve dans certains de ces fichiers :

runtests.py
#!/usr/bin/env python
import os
import sys

import django
from django.conf import settings
from django.test.utils import get_runner

if __name__ == "__main__":
    os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
    django.setup()
    TestRunner = get_runner(settings)
    test_runner = TestRunner()
    failures = test_runner.run_tests(["tests"])
    sys.exit(bool(failures))

Il s’agit ici du script invoqué pour lancer la suite de tests. Il met en place l’environnement Django, crée la base de données de tests et lance les tests.

Par souci de clarté, cet exemple ne contient que le strict minimum nécessaire à l’utilisation du lanceur de tests de Django. On peut cependant imaginer ajouter des options en ligne de commande pour le contrôle de la verbosité, pour passer des étiquettes de tests spécifiques à lancer, etc.

tests/test_settings.py
SECRET_KEY = 'fake-key'
INSTALLED_APPS = [
    "tests",
]

Ce fichier contient les réglages Django indispensables pour lancer les tests de votre application.

Encore une fois, il s’agit d’un exemple minimal ; les tests peuvent exiger des réglages supplémentaires pour fonctionner.

Comme le paquet tests est inclus dans INSTALLED_APPS au moment de lancer les tests, il est possible de définir des modèles uniquement pour les tests dans le fichier models.py.

Utilisation d’autres infrastructures de test

Il est clair que unittest n’est pas la seule infrastructure de test de Python. Même si Django ne fournit pas de prise en charge explicite d’autres systèmes de test, il offre une manière d’invoquer des tests construits pour un autre système comme s’il s’agissait de tests Django normaux.

Lorsque vous lancez ./manage.py test, Django consulte le réglage TEST_RUNNER pour déterminer ce qu’il doit faire. Par défaut, TEST_RUNNER contient 'django.test.runner.DiscoverRunner'. Cette classe définit le comportement de test par défaut de Django. Ce comportement implique :

  1. Une préparation générale avant les tests.

  2. La recherche de tests dans tout fichier au-dessous du répertoire actuel dont le nom correspond au motif test*.py.

  3. La création des bases de données de test.

  4. Le lancement de migrate pour installer les modèles et les données initiales dans les bases de données de test.

  5. L’exécution des tests découverts.

  6. La suppression des bases de données de test.

  7. Une finalisation générale après les tests.

Si vous définissez votre propre classe de lanceur de tests et que vous indiquez cette classe dans TEST_RUNNER, Django se servira de votre lanceur de tests lors de chaque appel à ./manage.py test. De cette façon, il est possible d’utiliser n’importe quel système de test pouvant être lancé à partir de code Python ou de modifier le processus d’exécution des tests Django pour adapter les tests à d’éventuels besoins spécifiques.

Définition d’un lanceur de tests

Un lanceur de tests est une classe définissant une méthode run_tests(). Django fournit une classe DiscoverRunner définissant le comportement de test par défaut de Django. Cette classe définit le point d’entrée run_tests() ainsi qu’une sélection d’autres méthodes utilisées par run_tests() pour préparer, exécuter et clore la suite de tests.

class DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_sql=False, **kwargs)[source]

DiscoverRunner recherche des tests dans tout fichier correspondant au motif pattern.

top_level peut être utilisé pour indiquer le répertoire contenant les modules Python de premier niveau. Django peut généralement le découvrir automatiquement, ce qui fait que cette option n’est pas absolument nécessaire. Lorsqu’elle est définie, elle devrait en principe correspondre au répertoire contenant le fichier manage.py.

verbosity détermine la quantité de notifications et d’informations de débogage qui s’afficheront sur la console ; 0 n’affiche rien, 1 correspond à l’affichage normal et 2 à l’affichage verbeux.

Si interactive vaut True, la suite de tests a le droit de poser des questions à l’utilisateur durant son exécution. Par exemple, elle pourrait demander la permission de supprimer une base de données de test existante. Si interactive vaut False, la suite de tests doit pouvoir s’exécuter sans aucune intervention manuelle de l’utilisateur.

Si failfast vaut True, la suite de tests s’arrête immédiatement après le premier échec de test.

Si keepdb vaut True, la suite de tests va utiliser la base de données existante ou en créer une si c’est nécessaire. Si la valeur est False, une nouvelle base de données est toujours créée, demandant à l’utilisateur de supprimer une éventuelle base existante de même nom.

Si reverse vaut True, les cas de tests sont exécutés dans l’ordre inverse. Cela peut être utile pour déboguer des tests qui ne sont pas isolés correctement et génèrent des effets de bord. Le groupement par classe de test est conservé lors de l’utilisation de cette option.

Si debug_sql vaut True, les tests en échec affichent les requêtes SQL journalisées vers django.db.backends en plus de la pile d’appels. Si verbosity vaut 2, les requêtes de tous les tests sont affichées.

Il est possible que de temps à autre, Django étende les capacités du lanceur de tests en ajoutant de nouveaux paramètres. La déclaration **kwargs permet cette extension. Si vous héritez de DiscoverRunner ou que vous écrivez votre propre lanceur de tests, vérifiez qu’il accepte **kwargs.

Votre lanceur de tests peut aussi définir des options supplémentaires en ligne de commande. Créez ou surchargez une méthode de classe add_arguments(cls, parser) et ajoutez des paramètres personnalisés en appelant parser.add_argument() dans cette méthode, afin que la commande test puisse exploiter ces paramètres.

Attributs

DiscoverRunner.test_suite

La classe utilisée pour construire la suite de tests. Par défaut, il s’agit de unittest.TestSuite. Il est possible de surcharger cette valeur si vous souhaitez implémenter une logique différente pour collecter les tests.

DiscoverRunner.test_runner

Cette classe est celle du lanceur de tests de bas niveau utilisé pour exécuter les tests individuels et mettre en forme les résultats. Par défaut, il s’agit de unittest.TextTestRunner. Malgré la malencontreuse similitude des conventions de nommage, il ne s’agit pas du même type de classe que DiscoverRunner, qui prend en charge un plus grand spectre de responsabilités. Vous pouvez surcharger cet attribut pour modifier la façon dont les tests sont exécutés et leurs résultats affichés.

DiscoverRunner.test_loader

Il s’agit de la classe qui charge les tests, que ce soit à partir de classes TestCase, de modules ou d’autres façons, et qui les consolide en suites de tests que le lanceur pourra exécuter. Par défaut, c’est unittest.defaultTestLoader. Vous pouvez surcharger cet attribut si vos tests doivent être chargés de manière inhabituelle.

Méthodes

DiscoverRunner.run_tests(test_labels, extra_tests=None, **kwargs)[source]

Lance la suite de tests.

test_labels permet d’indiquer les tests à exécuter et accepte plusieurs formats (voir DiscoverRunner.build_suite() pour la liste des formats pris en charge).

extra_tests est une liste d’instances TestCase supplémentaires à ajouter à la suite exécutée par le lanceur de tests. Ces tests supplémentaires sont exécutés en plus de ceux découverts dans les modules énumérés dans test_labels.

Cette méthode doit renvoyer le nombre de tests ayant échoué.

classmethod DiscoverRunner.add_arguments(parser)[source]

Surchargez cette méthode de classe pour ajouter des paramètres personnalisés acceptés par la commande d’administration test. Voir argparse.ArgumentParser.add_argument() pour plus de détails au sujet de l’ajout de paramètres de ligne de commande.

DiscoverRunner.setup_test_environment(**kwargs)[source]

Met en place l’environnement de test en appelant setup_test_environment() et en définissant DEBUG à False.

DiscoverRunner.build_suite(test_labels, extra_tests=None, **kwargs)[source]

Construit une suite de tests correspondant aux noms de test indiqués.

test_labels est une liste de chaînes décrivant les tests à exécuter. Un nom de test peut apparaître sous quatre formes :

  • chemin.vers.module_test.TestCase.methode_test – Lance une seule méthode de test dans un cas de test.

  • chemin.vers.module_test.TestCase – Lance toutes les méthodes de test d’un cas de test.

  • chemin.vers.module – Recherche et lance tous les tests d’un paquet ou module Python donné.

  • chemin/vers/répertoire – Recherche et lance tous les tests dans la sous-arborescence du répertoire donné.

Si test_labels contient la valeur None, le lanceur de tests recherche des tests dans tous les fichiers au-dessous du répertoire actuel dont le nom correspond à son motif pattern (voir ci-dessus).

extra_tests est une liste d’instances TestCase supplémentaires à ajouter à la suite exécutée par le lanceur de tests. Ces tests supplémentaires sont exécutés en plus de ceux découverts dans les modules énumérés dans test_labels.

Renvoie une instance TestSuite prête à être exécutée.

DiscoverRunner.setup_databases(**kwargs)[source]

Crée les bases de données de test.

Renvoie une structure de données fournissant suffisamment de détails pour annuler les changements effectués. Ces données sont ensuite transmises à la fonction teardown_databases() lors de la finalisation des tests.

DiscoverRunner.run_suite(suite, **kwargs)[source]

Lance la suite de tests.

Renvoie le résultat produit par l’exécution de la suite de tests.

DiscoverRunner.teardown_databases(old_config, **kwargs)[source]

Supprime les bases de données de test, rétablissant la situation d’avant-test.

old_config est une structure de données définissant les modifications de la configuration des bases de données qui doivent être annulées. C’est la valeur de renvoi de la méthode setup_databases().

DiscoverRunner.teardown_test_environment(**kwargs)[source]

Rétablit l’environnement à son état d’avant les tests.

DiscoverRunner.suite_result(suite, result, **kwargs)[source]

Calcule et renvoie un code de retour basé sur une suite de tests et des résultats de ces tests.

Utilitaires de test

django.test.utils

Pour aider à la création de votre propre lanceur de tests, Django fournit un certain nombre de méthodes utilitaires dans le module django.test.utils.

setup_test_environment()[source]

Effectue la mise en place générale avant les tests, comme l’installation des adaptations du système de rendu des gabarits et la mise en place de la boîte de messagerie factice.

teardown_test_environment()[source]

Effectue la finalisation générale après les tests, comme la suppression des adaptations du système des gabarits et le rétablissement des services de messagerie par défaut.

django.db.connection.creation

Le module de création du moteur de base de données propose également quelques utilitaires pouvant être utiles durant les tests.

create_test_db(verbosity=1, autoclobber=False, serialize=True, keepdb=False)

Crée une nouvelle base de données de test et exécute migrate sur cette base.

verbosity a le même effet que dans run_tests().

autoclobber décrit le comportement adopté si une base de données de même nom que la base de données de test existe déjà :

  • Si autoclobber vaut False, l’utilisateur devra confirmer la suppression de la base de données existante. sys.exit est appelée si l’utilisateur refuse.

  • Si autoclobber vaut True, la base de données est supprimée sans rien demander à l’utilisateur.

serialize détermine si Django sérialise la base de données en une chaîne JSON en mémoire avant de lancer les tests (utilisé pour restaurer l’état de la base de données entre les tests s’il n’y a pas de transactions). Vous pouvez le définir à False pour accélérer nettement le temps de création si aucune de vos classes de test ne comporte serialized_rollback=True.

Si vous utilisez le lanceur de tests par défaut, vous pouvez contrôler cela avec la clé SERIALIZE du dictionnaire TEST.

keepdb détermine si la session de test doit utiliser une base de données existante ou si elle doit en créer une nouvelle. Si True, une éventuelle base de données existante est utilisée, sinon une nouvelle base est créée. Si False, une nouvelle base de données sera créée dans tous les cas, en demandant à l’utilisateur s’il doit supprimer la base existante, s’il y en a une.

Renvoie le nom de la base de données de test qui a été créée.

create_test_db() présente un effet de bord : la valeur de NAME dans DATABASES est mise à jour pour correspondre au nom de la base de données de test.

destroy_test_db(old_database_name, verbosity=1, keepdb=False)

Supprime la base de données dont le nom correspond à la valeur de NAME dans DATABASES et met à jour NAME avec la valeur contenue dans old_database_name.

Le paramètre verbosity a le même effet que pour DiscoverRunner.

Si le paramètre keepdb vaut True, la connexion à la base de données sera fermée, mais la base de données ne sera pas détruite.

Intégration avec coverage.py

La couverture de code représente la quantité de code source mis à l’épreuve par les tests. Elle montre quelles sont les parties de code que les tests éprouvent et les parties qui ne le sont pas. C’est un aspect important du test des applications, c’est pourquoi il est fortement recommandé de contrôler la couverture de vos tests.

Django peut facilement s’intégrer à coverage.py, un outil de mesure de couverture du code de programmes Python. Premièrement, installez coverage.py. Puis, lancez la commande suivante à partir du répertoire de votre projet contenant manage.py:

coverage run --source='.' manage.py test myapp

Cela va lancer les tests et collecter les données de couverture des fichiers exécutés dans votre projet. Vous pouvez afficher un rapport de ces données en saisissant la commande suivante :

coverage report

Notez que du code Django a été exécuté lors de l’exécution des tests, mais il ne figure pas dans le rapport à cause de l’option source indiquée dans la commande précédente.

Pour davantage d’options comme les listings HTML annotés détaillant les lignes manquantes, consultez la documentation de coverage.py.

Back to Top