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() et options().
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.
L’exemple suivant est un test unitaire simple utilisant la fabrique de requêtes :
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.client import RequestFactory
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
# Test my_view() as if it were deployed at /customer/details
response = my_view(request)
self.assertEqual(response.status_code, 200)
Si vous testez une configuration avec plusieurs bases de données impliquant une réplication maître/esclave, 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 le maître ne seront pas visibles sur l’esclave.
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': 'dbmaster',
# ... plus some other settings
},
'slave': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myproject',
'HOST': 'dbslave',
'TEST_MIRROR': 'default'
# ... plus some other settings
}
}
Dans cette configuration figurent deux serveurs de base de données : dbmaster, défini par l’alias de base de données default et dbslave, défini par l’alias slave. Comme l’on peut s’y attendre, dbslave a été configuré par l’administrateur de base de données comme esclave en lecture de dbmaster. En temps normal, toute écriture sur default apparaît aussi sur slave.
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 slave a été configurée comme miroir de test (via le réglage TEST_MIRROR), indiquant par là que lors des tests, slave 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 slave. Par contre, la connexion vers slave sera redirigée pour pointer vers default. Par conséquent, les écritures sur default apparaîtront aussi sur slave, 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.
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 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 TEST_DEPENDENCIES comporte des dépendances circulaires, une exception ImproperlyConfigured sera générée.
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_syncdb 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_syncdb 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_syncdb 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_syncdb 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_syncdb 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.
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.
Si vous souhaitez lancer des tests indépendamment de ./manage.py test, par exemple à partir d’une invite de commande, vous devez d’abord mettre en place l’environnement de test. Django fournit une méthode à cet effet :
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment() place plusieurs fonctionnalités de Django en mode de test, mais ne crée pas de base de données de test ; c’est django.test.runner.DiscoverRunner.setup_databases() qui se charge de cela.
L’appel à setup_test_environment() est effectué automatiquement dans le cadre de la mise en place des tests par ./manage.py test. Cette méthode ne doit être manuellement appelée que si vous lancez vos tests d’une autre façon que par le lanceur de tests de Django.
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 :
Une préparation générale avant les tests.
La recherche de tests dans tout fichier au-dessous du répertoire actuel dont le nom correspond au motif test*.py.
La création des bases de données de test.
Le lancement de syncdb pour installer les modèles et les données initiales dans les bases de données de test.
L’exécution des tests découverts.
La suppression des bases de données de test.
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.
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.
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.
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 de ligne de commande supplémentaires. Si vous ajoutez un attribut option_list à une sous-classe du lanceur de tests, ces options sont ajoutées à la liste des options en ligne de commande que la commande test peut utiliser.
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.
Il s’agit du tuple des options optparse qui seront fournies à la classe OptionParser de la commande de gestion pour analyser les paramètres reçus. Consultez la documentation du module optparse de Python pour plus de détails.
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é.
Met en place l’environnement de test en appelant setup_test_environment() et en définissant DEBUG à False.
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.
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.
Lance la suite de tests.
Renvoie le résultat produit par l’exécution de la suite de tests.
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().
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.
Le module de création du moteur de base de données propose également quelques utilitaires pouvant être utiles durant les tests.
Crée une nouvelle base de données de test et exécute syncdb 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.
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.
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.
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.
Jan 13, 2016