Comment fournir des montages de test pour les modèles Django dans Pytest – Real Python

By | avril 8, 2020

Cours Python en ligne

Si vous travaillez à Django, pytest Les appareils peuvent vous aider à créer des tests pour vos modèles qui ne sont pas compliqués à entretenir. Écrire de bons tests est une étape cruciale pour maintenir une application réussie, et agencements sont un ingrédient clé pour rendre votre suite de tests efficace et efficiente. Les appareils sont de petites données qui servent de référence à vos tests.

Au fur et à mesure que vos scénarios de test changent, il peut être difficile d'ajouter, de modifier et d'entretenir vos appareils. Mais ne vous inquiétez pas. Ce tutoriel vous montrera comment utiliser le pytest-django plugin pour faire de l'écriture de nouveaux cas de test et de nouveaux appareils un jeu d'enfant.

Dans ce didacticiel, vous apprendrez:

  • Comment créer et charger montages d'essai à Django
  • Comment créer et charger pytest fixtures pour les modèles Django
  • Comment utiliser des usines pour créer des montages de test pour les modèles Django pytest
  • Comment créer des dépendances entre des appareils de test à l'aide du usine comme luminaire modèle

Les concepts décrits dans ce didacticiel conviennent à tout projet Python utilisant pytest. Pour plus de commodité, les exemples utilisent l'ORM de Django, mais les résultats peuvent être reproduits dans d'autres types d'ORM et même dans des projets qui n'utilisent pas d'ORM ou de base de données.

Calendrier à Django

Pour commencer, vous allez mettre en place un nouveau projet Django. Tout au long de ce didacticiel, vous écrirez des tests à l'aide du module d'authentification intégré.

Configuration d'un environnement virtuel Python

Lorsque vous créez un nouveau projet, il est préférable de créer également un environnement virtuel pour celui-ci. Un environnement virtuel vous permet d'isoler le projet des autres projets sur votre ordinateur. De cette façon, différents projets peuvent utiliser différentes versions de Python, Django ou tout autre package sans interférer les uns avec les autres.

Voici comment créer votre environnement virtuel dans un nouveau répertoire:

$ mkdir django_fixtures
$ CD django_fixtures
django_fixtures $ python -m venv venv

Pour obtenir des instructions détaillées sur la façon de créer un environnement virtuel, consultez Python Virtual Environments: A Primer.

L'exécution de cette commande créera un nouveau répertoire appelé venv. Ce répertoire stockera tous les packages que vous installez dans l'environnement virtuel.

Configuration d'un projet Django

Maintenant que vous avez un nouvel environnement virtuel, il est temps de configurer un projet Django. Dans votre terminal, activez l'environnement virtuel et installez Django:

$ la source venv / bin / activate
$ installer pip django

Maintenant que Django est installé, vous pouvez créer un nouveau projet Django appelé django_fixtures:

$ django-admin startproject django_fixtures

Après avoir exécuté cette commande, vous verrez que Django a créé de nouveaux fichiers et répertoires. Pour en savoir plus sur la façon de démarrer un nouveau projet Django, consultez Démarrage d'un projet Django.

Pour terminer la configuration de votre projet Django, appliquez le migrations pour les modules intégrés:

$ CD django_fixtures
$ migration de python manage.py
Opérations à effectuer:
        Appliquer toutes les migrations: admin, auth, contenttypes, sessions
Exécution de migrations:
        Application de contenttypes.0001_initial ... OK
        Appliquer auth.0001_initial ... OK
        Application de admin.0001_initial ... OK
        Application de admin.0002_logentry_remove_auto_add ... OK
        Application de admin.0003_logentry_add_action_flag_choices ... OK
        Application de contenttypes.0002_remove_content_type_name ... OK
        Appliquer auth.0002_alter_permission_name_max_length ... OK
        Appliquer auth.0003_alter_user_email_max_length ... OK
        Appliquer auth.0004_alter_user_username_opts ... OK
        Appliquer auth.0005_alter_user_last_login_null ... OK
        Appliquer auth.0006_require_contenttypes_0002 ... OK
        Application de auth.0007_alter_validators_add_error_messages ... OK
        Appliquer auth.0008_alter_user_username_max_length ... OK
        Appliquer auth.0009_alter_user_last_name_max_length ... OK
        Appliquer auth.0010_alter_group_name_max_length ... OK
        Application d'auth.0011_update_proxy_permissions ... OK
        Application de sessions.0001_initial ... OK

La sortie répertorie toutes les migrations appliquées par Django. Lors du démarrage d'un nouveau projet, Django applique des migrations pour les applications intégrées telles que auth, séances, et administrateur.

Vous êtes maintenant prêt à commencer à écrire des tests et des fixtures!

Création d'appareils Django

Django fournit sa propre façon de créer et de charger des appareils pour les modèles à partir de fichiers. Les fichiers des appareils Django peuvent être écrits en JSON ou YAML. Dans ce didacticiel, vous allez travailler avec le format JSON.

La façon la plus simple de créer un appareil Django est d'utiliser un objet existant. Démarrez un shell Django:

$ shell python manage.py
Python 3.8.0 (par défaut, 23 octobre 2019, 18:51:26)
[GCC 9.2.0]    sous linux
Tapez "aide", "copyright", "crédits" ou "licence" pour plus d'informations.
(InteractiveConsole)

A l'intérieur du shell Django, créez un nouveau groupe appelé appuseurs:

>>>

>>> de django.contrib.auth.models importation Groupe
>>> groupe = Groupe.objets.créer(Nom="appusers")
>>> groupe.pk
1

le Groupe Le modèle fait partie du système d'authentification de Django. Les groupes sont très utiles pour gérer les autorisations dans un projet Django.

Vous avez créé un nouveau groupe appelé appuseurs. le clé primaire du groupe que vous venez de créer est 1. Pour créer un appareil pour le groupe appuseurs, vous allez utiliser la commande de gestion Django dumpdata.

Quittez le shell Django avec sortie() et exécutez la commande suivante depuis votre terminal:

$ python manage.py dumpdata auth.Group --pk 1 --indent 4 > group.json

Dans cet exemple, vous utilisez le dumpdata pour générer des fichiers de luminaires à partir d'instances de modèle existantes. Décomposons-le:

  • groupe d'authentification: Décrit le modèle à vider. Le format est ..

  • --pk 1: Décrit l'objet à vider. La valeur est une liste de clés primaires séparées par des virgules, telles que 1,2,3.

  • --indent 4: Il s'agit d'un argument de formatage facultatif qui indique à Django le nombre d'espaces à ajouter avant chaque niveau d'indentation dans le fichier généré. L'utilisation d'indentations rend le fichier d'installation plus lisible.

  • > group.json: Décrit où écrire la sortie de la commande. Dans ce cas, la sortie sera écrite dans un fichier appelé group.json.

Ensuite, inspectez le contenu du fichier de fixture group.json:

[[[[

    "modèle": "auth.group",
    "pk": 1,
    "des champs": 
        "Nom": "appusers",
        "autorisations": []
    

]

Le fichier fixture contient une liste d'objets. Dans ce cas, vous n'avez qu'un seul objet dans la liste. Chaque objet comprend un entête avec le nom du modèle et la clé primaire, ainsi qu'un dictionnaire avec la valeur de chaque champ du modèle. Vous pouvez voir que le luminaire contient le nom du groupe appuseurs.

Vous pouvez créer et éditer des fichiers de fixations manuellement, mais il est généralement plus pratique de créer l’objet au préalable et d’utiliser Django dumpdata pour créer le fichier de fixture.

Chargement des appareils Django

Maintenant que vous avez un fichier fixture, vous voulez le charger dans la base de données. Mais avant de faire cela, vous devez ouvrir un shell Django et supprimer le groupe que vous avez déjà créé:

>>>

>>> de django.contrib.auth.models importation Groupe
>>> Groupe.objets.filtre(pk=1).supprimer()
(1, 'auth.Group_permissions': 0, 'auth.User_groups': 0, 'auth.Group': 1)

Maintenant que le groupe est supprimé, chargez le projecteur à l'aide du données de charge commander:

$ python manage.py loaddata group.json
Installé 1 objet (s) à partir de 1 luminaire (s)

Pour vous assurer que le nouveau groupe a été chargé, ouvrez un shell Django et récupérez-le:

>>>

>>> de django.contrib.auth.models importation Groupe
>>> groupe = Groupe.objets.avoir(pk=1)
>>> vars(groupe)
'_Etat': ,
    'id': 1,
    'name': 'appusers'

Génial! Le groupe était chargé. Vous venez de créer et de charger votre premier appareil Django.

Chargement des appareils Django dans les tests

Jusqu'à présent, vous avez créé et chargé un fichier de luminaire à partir de la ligne de commande. Maintenant, comment pouvez-vous l'utiliser pour les tests? Pour voir comment les appareils sont utilisés dans les tests Django, créez un nouveau fichier appelé test.pyet ajoutez le test suivant:

de django.test importation Cas de test
de django.contrib.auth.models importation Groupe

classe Mon test(Cas de test):
    def test_should_create_group(soi):
        groupe = Groupe.objets.avoir(pk=1)
        soi.assertEqual(groupe.Nom, "appusers")

Le test récupère le groupe avec la clé primaire 1 et tester que son nom est appuseurs.

Exécutez le test depuis votre terminal:

$ python manage.py tester tester
Création d'une base de données de test pour l'alias 'par défaut' ...
La vérification du système n'a identifié aucun problème (0 désactivé).
E
================================================== ====================
ERREUR: test_should_create_group (test.MyTest)
-------------------------------------------------- --------------------
Traceback (dernier appel le plus récent):
        Fichier "/django_fixtures/django_fixtures/test.py", ligne 9, dans test_should_create_group
                group = Group.objects.get (pk = 1)
        Fichier "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/manager.py", ligne 82, dans manager_method
                return getattr (self.get_queryset (), name) (* args, ** kwargs)
        Fichier "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/query.py", ligne 415, dans get
                augmenter self.model.DoesNotExist (
django.contrib.auth.models.Group.DoesNotExist: la requête de correspondance de groupe n'existe pas.

-------------------------------------------------- --------------------
Test Ran 1 en 0,001s

ÉCHEC (erreurs = 1)
Détruire la base de données de test pour l'alias 'par défaut' ...

Le test a échoué car un groupe avec la clé primaire 1 n'existe pas.

Pour charger le luminaire dans le test, vous pouvez utiliser un attribut spécial de la classe Cas de test appelé agencements:

de django.test importation Cas de test
de django.contrib.auth.models importation Groupe

classe Mon test(Cas de test):
    agencements = [[[["group.json"]

    def test_should_create_group(soi):
        groupe = Groupe.objets.avoir(pk=1)
        soi.assertEqual(groupe.Nom, "appusers")

Ajout de cet attribut à un Cas de test indique à Django de charger les appareils avant d'exécuter chaque test. Remarquerez que agencements accepte un tableau, vous pouvez donc fournir plusieurs fichiers de luminaire à charger avant chaque test.

L'exécution du test produit désormais la sortie suivante:

$ python manage.py tester tester
Création d'une base de données de test pour l'alias 'par défaut' ...
La vérification du système n'a identifié aucun problème (0 désactivé).
.
-------------------------------------------------- --------------------
Test Ran 1 en 0,005 s

D'accord
Détruire la base de données de test pour l'alias 'par défaut' ...

Incroyable! Le groupe a été chargé et le test a réussi. Vous pouvez maintenant utiliser le groupe appuseurs dans vos tests.

Jusqu'à présent, vous avez utilisé un seul fichier avec un seul objet. Cependant, la plupart du temps, votre application comporte de nombreux modèles et vous aurez besoin de plusieurs modèles dans un test.

Pour voir à quoi ressemblent les dépendances entre les objets dans les appareils Django, créez une nouvelle instance utilisateur, puis ajoutez-la à la appuseurs groupe que vous avez créé auparavant:

>>>

>>> de django.contrib.auth.models importation Utilisateur, Groupe
>>> appuseurs = Groupe.objets.avoir(Nom="appusers")
>>> haki = Utilisateur.objets.Créer un utilisateur("haki")
>>> haki.pk
1
>>> haki.groupes.ajouter(appuseurs)

L'utilisateur haki est maintenant membre du appuseurs groupe. Pour voir à quoi ressemble un appareil avec une clé étrangère, générez un appareil pour l'utilisateur 1:

$ python manage.py dumpdata auth.User --pk 1 --indent 4
[[[[

                "model": "auth.user",
                "pk": 1,
                "des champs": 
                                "mot de passe": "! M4dygH3ZWfd0214U59OR9nlwsRJ94HUZtvQciG8y",
                                "last_login": null,
                                "is_superuser": false,
                                "nom d'utilisateur": "haki",
                                "Prénom": "",
                                "nom de famille": "",
                                "email": "",
                                "is_staff": false,
                                "is_active": vrai,
                                "date_joined": "2019-12-07T09: 32: 50.998Z",
                                "groupes":[[[[
                                                1
                                ],
                                "user_permissions": []
                

]

La structure du luminaire est similaire à celle que vous avez vue précédemment.

Un utilisateur peut être associé à plusieurs groupes, de sorte que le champ groupe contient les ID de tous les groupes auxquels appartient l'utilisateur. Dans ce cas, l'utilisateur appartient au groupe avec la clé primaire 1, qui est votre appuseurs groupe.

L'utilisation de clés primaires pour référencer des objets dans des appareils n'est pas toujours une bonne idée. La clé primaire d'un groupe est un identifiant arbitraire que la base de données attribue au groupe lors de sa création. Dans un autre environnement ou sur un autre ordinateur, le appuseurs Le groupe peut avoir un ID différent et cela ne fera aucune différence sur l'objet.

Pour éviter d'utiliser des identifiants arbitraires, Django définit le concept de clés naturelles. Une clé naturelle est un identifiant unique d'un objet qui n'est pas nécessairement la clé primaire. Dans le cas des groupes, deux groupes ne peuvent pas avoir le même nom, donc une clé naturelle pour le groupe peut être son nom.

Pour utiliser des clés naturelles au lieu de clés primaires pour référencer des objets associés dans un appareil Django, ajoutez le - naturel-étranger drapeau au dumpdata commander:

$ python manage.py dumpdata auth.User --pk 1 --indent 4 - naturel-étranger
[[[[

                "model": "auth.user",
                "pk": 1,
                "des champs": 
                                "mot de passe": "! f4dygH3ZWfd0214X59OR9ndwsRJ94HUZ6vQciG8y",
                                "last_login": null,
                                "is_superuser": false,
                                "nom d'utilisateur": "haki",
                                "Prénom": "",
                                "nom de famille": "",
                                "email": "benita",
                                "is_staff": false,
                                "is_active": vrai,
                                "date_joined": "2019-12-07T09: 32: 50.998Z",
                                "groupes":[[[[
                                                [[[[
                                                                `appusers`
                                                ]
                                ],
                                "user_permissions": []
                

]

Django a généré le fixture pour l'utilisateur, mais au lieu d'utiliser la clé primaire du appuseurs groupe, il a utilisé le nom du groupe.

Vous pouvez également ajouter le - naturel-primaire pour exclure la clé primaire d'un objet du luminaire. Quand pk est null, la clé primaire sera définie lors de l'exécution, généralement par la base de données.

Maintenance des appareils Django

Les appareils Django sont excellents, mais ils posent également certains défis:

  • Garder les appareils mis à jour: Les appareils Django doivent contenir tous les champs obligatoires du modèle. Si vous ajoutez un nouveau champ qui ne peut pas être annulé, vous devez mettre à jour les appareils. Sinon, leur chargement échouera. Garder les appareils Django à jour peut devenir un fardeau quand vous en avez beaucoup.

  • Maintenir les dépendances entre les appareils: Les appareils Django qui dépendent d'autres appareils doivent être chargés ensemble et dans un ordre particulier. Rester à jour avec les nouveaux cas de test ajoutés et les anciens cas de test modifiés peut être difficile.

Pour ces raisons, les luminaires Django ne sont pas un choix idéal pour les modèles qui changent souvent. Par exemple, il serait très difficile de gérer les appareils Django pour les modèles qui sont utilisés pour représenter les objets principaux dans l'application tels que les ventes, les commandes, les transactions ou les réservations.

D'autre part, les appareils Django sont une excellente option pour les cas d'utilisation suivants:

  • Données constantes: Cela s'applique aux modèles qui changent rarement, tels que les codes de pays et les codes postaux.

  • Donnée initiale: Cela s'applique aux modèles qui stockent les données de recherche de votre application, telles que les catégories de produits, les groupes d'utilisateurs et les types d'utilisateurs.

pytest Calendrier à Django

Dans la section précédente, vous avez utilisé les outils intégrés fournis par Django pour créer et charger des appareils. Les luminaires fournis par Django sont parfaits pour certains cas d'utilisation, mais pas idéaux pour d'autres.

Dans cette section, vous allez expérimenter avec un type de luminaire très différent: le pytest fixation. pytest fournit un système de fixation très complet que vous pouvez utiliser pour créer une suite de tests fiable et maintenable.

Configuration pytest pour un projet Django

Pour commencer pytest, vous devez d'abord installer pytest et le plugin Django pour pytest. Exécutez les commandes suivantes dans votre terminal lorsque l'environnement virtuel est activé:

$ pip installer pytest
$ pip installer pytest-django

le pytest-django le plugin est maintenu par le pytest équipe de développement. Il fournit des outils utiles pour écrire des tests pour les projets Django en utilisant pytest.

Ensuite, vous devez laisser pytest savoir où il peut trouver les paramètres de votre projet Django. Créez un nouveau fichier dans le répertoire racine du projet appelé pytest.iniet ajoutez-y les lignes suivantes:

[pytest]
DJANGO_SETTINGS_MODULE=django_fixtures.settings

Ceci est la quantité minimale de configuration nécessaire pour faire pytest travailler avec votre projet Django. Il existe de nombreuses autres options de configuration, mais cela suffit pour commencer.

Enfin, pour tester votre configuration, remplacez le contenu de test.py avec ce test factice:

def test_foo():
    affirmer Vrai

Pour exécuter le test factice, utilisez le pytest commande depuis votre terminal:

$ pytest test.py
============================== la session de test démarre ================== =====
plateforme Linux - Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Paramètres Django: django_fixtures.settings (à partir du fichier ini)
rootdir: / django_fixtures, inifile: pytest.ini
plugins: django-3.5.1

test.py.
                                [100%]
============================= 1 passé en 0,05 s ================= =========

Vous venez de terminer la configuration d'un nouveau projet Django avec pytest! Vous êtes maintenant prêt à creuser plus profondément.

Pour en savoir plus sur la configuration pytest et écrire des tests, découvrez le développement piloté par les tests avec pytest.

Accès à la base de données à partir de tests

Dans cette section, vous allez écrire des tests à l'aide du module d'authentification intégré django.contrib.auth. Les modèles les plus connus de ce module sont Utilisateur et Groupe.

Pour commencer avec Django et pytest, écrivez un test pour vérifier si la fonction Créer un utilisateur() fourni par Django définit correctement le nom d'utilisateur:

de django.contrib.auth.models importation Utilisateur

def test_should_create_user_with_username() -> Aucun:
    utilisateur = Utilisateur.objets.Créer un utilisateur("Haki")
    affirmer utilisateur.Nom d'utilisateur == "Haki"

Maintenant, essayez d'exécuter le test à partir de votre commande comme:

$ pytest test.py
================================== la session de test démarre ============= ==
plateforme Linux - Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Paramètres Django: django_fixtures.settings (à partir du fichier ini)
rootdir: / django-django_fixtures / django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collecté 1 article

test.py F

=============================== FAILURES ================== ===========
____________________test_should_create_user_with_username ____________

                def test_should_create_user_with_username () -> Aucun:
>       utilisateur = User.objects.create_user("Haki")

soi = , nom = Aucun

                def _cursor (self, name = None):
>       self.ensure_connection()

E Échec: l'accès à la base de données n'est pas autorisé, utilisez la marque "django_db" ou la "db"
                                ou des appareils "transactional_db" pour l'activer.

La commande a échoué et le test ne s'est pas exécuté. Le message d'erreur vous donne quelques informations utiles: Pour accéder à la base de données dans un test, vous devez injecter un appareil spécial appelé db. le db luminaire fait partie de la django-pytest plug-in que vous avez installé précédemment, et il est nécessaire d'accéder à la base de données dans les tests.

Injectez le db montage dans le test:

de django.contrib.auth.models importation Utilisateur

def test_should_create_user_with_username(db) -> Aucun:
    utilisateur = Utilisateur.objets.Créer un utilisateur("Haki")
    affirmer utilisateur.Nom d'utilisateur == "Haki"

Relancez le test:

$ pytest test.py
================================== la session de test démarre ============= ==
plateforme Linux - Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Paramètres Django: django_fixtures.settings (à partir du fichier ini)
rootdir: / django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collecté 1 article

test.py.

Génial! La commande s'est terminée avec succès et votre test a réussi. Vous savez maintenant comment accéder à la base de données dans les tests. Vous avez également injecté un appareil dans un scénario de test en cours de route.

Création d'appareils pour les modèles Django

Maintenant que vous connaissez Django et pytest, écrivez un test pour vérifier qu'un mot de passe défini avec set_password () est validé comme prévu. Remplacez le contenu de test.py avec ce test:

de django.contrib.auth.models importation Utilisateur

def test_should_check_password(db) -> Aucun:
    utilisateur = Utilisateur.objets.Créer un utilisateur("UNE")
    utilisateur.set_password("secret")
    affirmer utilisateur.check_password("secret") est Vrai

def test_should_not_check_unusable_password(db) -> Aucun:
    utilisateur = Utilisateur.objets.Créer un utilisateur("UNE")
    utilisateur.set_password("secret")
    utilisateur.set_unusable_password()
    affirmer utilisateur.check_password("secret") est Faux

Le premier test vérifie qu'un utilisateur avec un mot de passe utilisable est en cours de validation par Django. Le deuxième test vérifie un cas limite dans lequel le mot de passe de l'utilisateur est inutilisable et ne doit pas être validé par Django.

Il y a une distinction importante à faire ici: les cas de test ci-dessus ne testent pas Créer un utilisateur(). Ils testent set_password (). Cela signifie un changement Créer un utilisateur() ne devrait pas affecter ces cas de test.

Notez également que le Utilisateur l'instance est créée deux fois, une fois pour chaque cas de test. Un grand projet peut avoir de nombreux tests qui nécessitent un Utilisateur exemple. Si chaque scénario de test crée son propre utilisateur, vous pourriez avoir des problèmes à l'avenir si le Utilisateur changements de modèle.

Pour réutiliser un objet dans de nombreux cas de test, vous pouvez créer un appareil de test:

importation pytest
de django.contrib.auth.models importation Utilisateur

@pytest.fixation
def user_A(db) -> Utilisateur:
    revenir Utilisateur.objets.Créer un utilisateur("UNE")

def test_should_check_password(db, user_A: Utilisateur) -> Aucun:
    user_A.set_password("secret")
    affirmer user_A.check_password("secret") est Vrai

def test_should_not_check_unusable_password(db, user_A: Utilisateur) -> Aucun:
    user_A.set_password("secret")
    user_A.set_unusable_password()
    affirmer user_A.check_password("secret") est Faux

Dans le code ci-dessus, vous avez créé une fonction appelée user_A () qui crée et renvoie un nouveau Utilisateur exemple. Pour marquer la fonction en tant que luminaire, vous l'avez décorée avec le pytest.fixture décorateur. Une fois qu'une fonction est marquée comme un appareil, elle peut être injectée dans des cas de test. Dans ce cas, vous avez injecté le luminaire user_A en deux cas de test.

Entretien des appareils lorsque les exigences changent

Supposons que vous ayez ajouté une nouvelle exigence à votre application et que chaque utilisateur doit désormais appartenir à un "app_user" groupe. Les utilisateurs de ce groupe peuvent afficher et mettre à jour leurs propres informations personnelles. Pour tester votre application, vous devez que vos utilisateurs de test appartiennent à "app_user" groupe aussi:

importation pytest
de django.contrib.auth.models importation Utilisateur, Groupe, Autorisation

@pytest.fixation
def user_A(db) -> Groupe:
    groupe = Groupe.objets.créer(Nom="app_user")
    change_user_permissions = Autorisation.objets.filtre(
        nom de code__in=[[[["changer d'utilisateur", "view_user"],
    )
    groupe.autorisations.ajouter(*change_user_permissions)
    utilisateur = Utilisateur.objets.Créer un utilisateur("UNE")
    utilisateur.groupes.ajouter(groupe)
    revenir utilisateur

def test_should_create_user(user_A: Utilisateur) -> Aucun:
    affirmer user_A.Nom d'utilisateur == "UNE"

def test_user_is_in_app_user_group(user_A: Utilisateur) -> Aucun:
    affirmer user_A.groupes.filtre(Nom="app_user").existe()

A l'intérieur du luminaire, vous avez créé le groupe "app_user" et a ajouté le changer d'utilisateur et view_user autorisations. Vous avez ensuite créé l'utilisateur test et l'avez ajouté au "app_user" groupe.

Auparavant, vous deviez parcourir chaque scénario de test qui avait créé un utilisateur et l'ajouter au groupe. En utilisant des appareils, vous avez pu effectuer le changement une seule fois. Une fois que vous avez changé le luminaire, le même changement est apparu dans chaque cas de test que vous avez injecté user_A dans. En utilisant des appareils, vous pouvez éviter les répétitions et rendre vos tests plus faciles à maintenir.

Injection d'appareils dans d'autres appareils

Les grandes applications ont généralement plus d'un seul utilisateur, et il est souvent nécessaire de les tester avec plusieurs utilisateurs. Dans cette situation, vous pouvez ajouter un autre appareil pour créer le test user_B:

importation pytest
de django.contrib.auth.models importation Utilisateur, Groupe, Autorisation

@pytest.fixation
def user_A(db) -> Utilisateur:
    groupe = Groupe.objets.créer(Nom="app_user")
    change_user_permissions = Autorisation.objets.filtre(
        nom de code__in=[[[["changer d'utilisateur", "view_user"],
    )
    groupe.autorisations.ajouter(*change_user_permissions)
    utilisateur = Utilisateur.objets.Créer un utilisateur("UNE")
    utilisateur.groupes.ajouter(groupe)
    revenir utilisateur

@pytest.fixation
def user_B(db) -> Utilisateur:
    groupe = Groupe.objets.créer(Nom="app_user")
    change_user_permissions = Autorisation.objets.filtre(
        nom de code__in=[[[["changer d'utilisateur", "view_user"],
    )
    groupe.autorisations.ajouter(*change_user_permissions)
    utilisateur = Utilisateur.objets.Créer un utilisateur("B")
    utilisateur.groupes.ajouter(groupe)
    revenir utilisateur

def test_should_create_two_users(user_A: Utilisateur, user_B: Utilisateur) -> Aucun:
    affirmer user_A.pk ! = user_B.pk

Dans votre terminal, essayez d'exécuter le test:

$ pytest test.py
==================== la session de test démarre =========================== ======
plateforme Linux - Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Paramètres Django: django_fixtures.settings (à partir du fichier ini)
rootdir: / django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collecté 1 article

test.py E
                              [100%]
============================= ERREURS ===================== ==================
_____________ ERREUR lors de la configuration de test_should_create_two_users ______________

soi = ,
sql = 'INSERT INTO "auth_group" ("name") VALUES (% s) RETURNING "auth_group". "id"'
, params = ('app_user',)

                def _execute (self, sql, params, * ignored_wrapper_args):
                                self.db.validate_no_broken_transaction ()
                                avec self.db.wrap_database_errors:
                                                si params est Aucun:
                                                                # params default peut être spécifique au backend.
                return self.cursor.execute (sql)
                                                autre:
>               revenir self.cursor.execute(sql, params)
E psycopg2.IntegrityError: la valeur de la clé en double est violée
                                                                contrainte unique "auth_group_name_key"
E DÉTAIL: La clé (nom) = (utilisateur_app) existe déjà.

======================== 1 erreur dans 4.14s ====================== ===========

Le nouveau test jette un IntegrityError. Le message d'erreur provient de la base de données, il peut donc sembler un peu différent selon la base de données que vous utilisez. Selon le message d'erreur, le test viole la contrainte unique sur le nom du groupe. Lorsque vous regardez vos appareils, cela a du sens. le "app_user" le groupe est créé deux fois, une fois dans le luminaire user_A et encore une fois dans le luminaire user_B.

Une observation intéressante que nous avons ignorée jusqu'à présent est que le luminaire user_A utilise le luminaire db. Cela signifie que les luminaires peuvent être injectés dans d'autres luminaires. Vous pouvez utiliser cette fonction pour IntegrityError au dessus de. Créez le "app_user" groupe une seule fois dans un appareil, et l'injecter à la fois dans le user_A et user_B agencements.

Pour ce faire, refactorisez votre test et ajoutez un "utilisateur de l'application" rencontre de groupe:

importation pytest
de django.contrib.auth.models importation Utilisateur, Groupe, Autorisation

@pytest.fixation
def app_user_group(db) -> Groupe:
    groupe = Groupe.objets.créer(Nom="app_user")
    change_user_permissions = Autorisation.objets.filtre(
        nom de code__in=[[[["changer d'utilisateur", "view_user"],
    )
    groupe.autorisations.ajouter(*change_user_permissions)
    revenir groupe

@pytest.fixation
def user_A(db, app_user_group: Groupe) -> Utilisateur:
    utilisateur = Utilisateur.objets.Créer un utilisateur("UNE")
    utilisateur.groupes.ajouter(app_user_group)
    revenir utilisateur

@pytest.fixation
def user_B(db, app_user_group: Groupe) -> Utilisateur:
    utilisateur = Utilisateur.objets.Créer un utilisateur("B")
    utilisateur.groupes.ajouter(app_user_group)
    revenir utilisateur

def test_should_create_two_users(user_A: Utilisateur, user_B: Utilisateur) -> Aucun:
    affirmer user_A.pk ! = user_B.pk

Dans votre terminal, exécutez vos tests:

$ pytest test.py
================================== la session de test démarre ============= ==
plateforme Linux - Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Paramètres Django: django_fixtures.settings (à partir du fichier ini)
rootdir: / django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collecté 1 article

test.py.

Incroyable! Vos tests réussissent. L'appareil de groupe résume la logique liée à la "utilisateur de l'application" groupe, comme la définition des autorisations. Vous avez ensuite injecté le groupe dans deux appareils utilisateur distincts. En construisant vos appareils de cette façon, vous avez rendu vos tests moins compliqués à lire et à maintenir.

Utiliser une usine

Jusqu'à présent, vous avez créé des objets avec très peu d'arguments. Cependant, certains objets peuvent être plus compliqués, comportant de nombreux arguments avec de nombreuses valeurs possibles. Pour de tels objets, vous souhaiterez peut-être créer plusieurs montages de test.

Par exemple, si vous fournissez tous les arguments à Créer un utilisateur(), voici à quoi ressemblerait le luminaire:

importation pytest
de django.contrib.auth.models importation Utilisateur

@pytest.fixation
def user_A(db, app_user_group: Groupe) -> Utilisateur
    utilisateur = Utilisateur.objets.Créer un utilisateur(
        Nom d'utilisateur="UNE",
        mot de passe="secret",
        Prénom="haki",
        nom de famille="benita",
        email="me@hakibenita.com",
        is_staff=Faux,
        is_superuser=Faux,
        c'est actif=Vrai,
    )
    utilisateur.groupes.ajouter(app_user_group)
    revenir utilisateur

Votre appareil est devenu beaucoup plus compliqué! Une instance d'utilisateur peut maintenant avoir de nombreuses variantes différentes, telles que superutilisateur, utilisateur personnel, utilisateur personnel inactif et utilisateur régulier inactif.

Dans les sections précédentes, vous avez appris qu'il peut être difficile de maintenir une logique de configuration compliquée dans chaque appareil de test. Donc, pour éviter d'avoir à répéter toutes les valeurs à chaque fois que vous créez un utilisateur, ajoutez une fonction qui utilise Créer un utilisateur() pour créer un utilisateur en fonction des besoins spécifiques de votre application:

de dactylographie importation liste, Optionnel
de django.contrib.auth.models importation Utilisateur, Groupe

def create_app_user(
    Nom d'utilisateur: str,
    mot de passe: Optionnel[[[[str] = Aucun,
    Prénom: Optionnel[[[[str] = "Prénom",
    nom de famille: Optionnel[[[[str] = "nom de famille",
    email: Optionnel[[[[str] = "foo@bar.com",
    is_staff: str = Faux,
    is_superuser: str = Faux,
    c'est actif: str = Vrai,
    groupes: liste[[[[Groupe] = [],
) -> Utilisateur:
    utilisateur = Utilisateur.objets.Créer un utilisateur(
        Nom d'utilisateur=Nom d'utilisateur,
        mot de passe=mot de passe,
        Prénom=Prénom,
        nom de famille=nom de famille,
        email=email,
        is_staff=is_staff,
        is_superuser=is_superuser,
        c'est actif=c'est actif,
    )
    utilisateur.groupes.ajouter(*groupes)
    revenir utilisateur

La fonction crée un utilisateur d'application. Chaque argument est défini avec une valeur par défaut sensible en fonction des besoins spécifiques de votre application. Par exemple, votre application peut exiger que chaque utilisateur ait une adresse e-mail, mais la fonction intégrée de Django n'applique pas une telle restriction. Vous pouvez à la place appliquer cette exigence dans votre fonction.

Les fonctions et classes qui créent des objets sont souvent appelées des usines. Pourquoi? C'est parce que ces fonctions agissent comme des usines qui produisent des instances d'une classe spécifique. Pour en savoir plus sur les usines en Python, consultez Le modèle de méthode d'usine et son implémentation en Python.

La fonction ci-dessus est une implémentation simple d'une usine. Il ne détient aucun état et ne met en œuvre aucune logique compliquée. Vous pouvez refactoriser vos tests afin qu'ils utilisent la fonction d'usine pour créer des instances utilisateur dans vos appareils:

@pytest.fixation
def user_A(db, app_user_group: Group) -> Utilisateur:
    revenir create_user(username="A", groups=[[[[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> Utilisateur:
    revenir create_user(username="B", groups=[[[[app_user_group])

def test_should_create_user(user_A: Utilisateur, app_user_group: Group) -> Aucun:
    assert user_A.username == "A"
    assert user_A.email == "foo@bar.com"
    assert user_A.groups.filter(pk=app_user_group.pk).existe()

def test_should_create_two_users(user_A: Utilisateur, user_B: Utilisateur) -> Aucun:
    assert user_A.pk != user_B.pk

Your fixtures got shorter, and your tests are now more resilient to change. For example, if you used a custom user model and you just added a new field to the model, you would only need to change create_user() for your tests to work as expected.

Using Factories as Fixtures

Complicated setup logic makes it harder to write and maintain tests, making the entire suite fragile and less resilient to change. So far, you’ve addressed this issue by creating fixtures, creating dependencies between fixtures, and using a factory to abstract as much of the setup logic as possible.

But there is still some setup logic left in your test fixtures:

@pytest.fixture
def user_A(db, app_user_group: Group) -> Utilisateur:
    revenir create_user(username="A", groups=[[[[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> Utilisateur:
    revenir create_user(username="B", groups=[[[[app_user_group])

Both fixtures are injected with app_user_group. This is currently necessary because the factory function create_user() does not have access to the app_user_group fixture. Having this setup logic in each test makes it harder to make changes, and it’s more likely to be overlooked in future tests. Instead, you want to encapsulate the entire process of creating a user and abstract it from the tests. This way, you can focus on the scenario at hand rather than setting up unique test data.

To provide the user factory with access to the app_user_group fixture, you can use a pattern called factory as fixture:

de typing import liste, Optionnel

import pytest
de django.contrib.auth.models import Utilisateur, Group, Autorisation

@pytest.fixture
def app_user_group(db) -> Group:
    group = Group.objets.create(name="app_user")
    change_user_permissions = Autorisation.objets.filter(
        codename__in=[[[["change_user", "view_user"],
    )
    group.autorisations.ajouter(*change_user_permissions)
    revenir group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
    # Closure
    def create_app_user(
        username: str,
        mot de passe: Optionnel[[[[str] = Aucun,
        first_name: Optionnel[[[[str] = "first name",
        last_name: Optionnel[[[[str] = "last name",
        email: Optionnel[[[[str] = "foo@bar.com",
        is_staff: str = False,
        is_superuser: str = False,
        is_active: str = Vrai,
        groups: liste[[[[Group] = [],
    ) -> Utilisateur:
        user = Utilisateur.objets.create_user(
            username=username,
            mot de passe=mot de passe,
            first_name=first_name,
            last_name=last_name,
            email=email,
            is_staff=is_staff,
            is_superuser=is_superuser,
            is_active=is_active,
        )
        user.groups.ajouter(app_user_group)
        # Add additional groups, if provided.
        user.groups.ajouter(*groups)
        revenir user
    revenir create_app_user

This is not far from what you’ve already done, so let’s break it down:

  • le app_user_group fixture remains the same. It creates the special "app user" group with all the necessary permissions.

  • A new fixture called app_user_factory is added, and it is injected with the app_user_group fixture.

  • The fixture app_user_factory creates a closure and returns an inner function called create_app_user().

  • create_app_user() is similar to the function you previously implemented, but now it has access to the fixture app_user_group. With access to the group, you can now add users to app_user_group in the factory function.

To use the app_user_factory fixture, inject it into another fixture and use it to create a user instance:

@pytest.fixture
def user_A(db, app_user_factory) -> Utilisateur:
    revenir app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> Utilisateur:
    revenir app_user_factory("B")

def test_should_create_user_in_app_user_group(
    user_A: Utilisateur,
    app_user_group: Group,
) -> Aucun:
    assert user_A.groups.filter(pk=app_user_group.pk).existe()

def test_should_create_two_users(user_A: Utilisateur, user_B: Utilisateur) -> Aucun:
    assert user_A.pk != user_B.pk

Notice that, unlike before, the fixture you created is providing a une fonction rather than an object. This is the main concept behind the factory as fixture pattern: The factory fixture creates a closure, which provides the inner function with access to fixtures.

For more about closures in Python, check out Python Inner Functions — What Are They Good For?

Now that you have your factories and fixtures, this is the complete code for your test:

de typing import liste, Optionnel

import pytest
de django.contrib.auth.models import Utilisateur, Group, Autorisation

@pytest.fixture
def app_user_group(db) -> Group:
    group = Group.objets.create(name="app_user")
    change_user_permissions = Autorisation.objets.filter(
        codename__in=[[[["change_user", "view_user"],
    )
    group.autorisations.ajouter(*change_user_permissions)
    revenir group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
    # Closure
    def create_app_user(
        username: str,
        mot de passe: Optionnel[[[[str] = Aucun,
        first_name: Optionnel[[[[str] = "first name",
        last_name: Optionnel[[[[str] = "last name",
        email: Optionnel[[[[str] = "foo@bar.com",
        is_staff: str = False,
        is_superuser: str = False,
        is_active: str = Vrai,
        groups: liste[[[[Group] = [],
    ) -> Utilisateur:
        user = Utilisateur.objets.create_user(
            username=username,
            mot de passe=mot de passe,
            first_name=first_name,
            last_name=last_name,
            email=email,
            is_staff=is_staff,
            is_superuser=is_superuser,
            is_active=is_active,
        )
        user.groups.ajouter(app_user_group)
        # Add additional groups, if provided.
        user.groups.ajouter(*groups)
        revenir user
    revenir create_app_user

@pytest.fixture
def user_A(db, app_user_factory) -> Utilisateur:
    revenir app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> Utilisateur:
    revenir app_user_factory("B")

def test_should_create_user_in_app_user_group(
    user_A: Utilisateur,
    app_user_group: Group,
) -> Aucun:
    assert user_A.groups.filter(pk=app_user_group.pk).existe()

def test_should_create_two_users(user_A: Utilisateur, user_B: Utilisateur) -> Aucun:
    assert user_A.pk != user_B.pk

Open the terminal and run the test:

$ pytest test.py
======================== test session starts ========================
platform linux -- Python 3.8.1, pytest-5.3.3, py-1.8.1, pluggy-0.13.1
django: settings: django_fixtures.settings (from ini)
rootdir: /django_fixtures/django_fixtures, inifile: pytest.ini
plugins: django-3.8.0
collected 2 items

test.py ..                                                     [100%]

======================== 2 passed in 0.17s ==========================

Great job! You’ve successfully implemented the factory as fixture pattern in your tests.

Factories as Fixtures in Practice

The factory as fixture pattern is very useful. So useful, in fact, that you can find it in the fixtures provided by pytest lui-même. For example, the tmp_path fixture provided by pytest is created by the fixture factory tmp_path_factory. Likewise, the tmpdir fixture is created by the fixture factory tmpdir_factory.

Mastering the factory as fixture pattern can eliminate many of the headaches associated with writing and maintaining tests.

Conclusion

You’ve successfully implemented a fixture factory that provides Django model instances. You’ve also maintained and implemented dependencies between fixtures in a way that takes some of the hassle out of writing and maintaining tests.

In this tutorial, you’ve learned:

  • How to create and load fixtures in Django
  • Comment fournir test fixtures for Django models in pytest
  • How to use des usines to create fixtures for Django models in pytest
  • How to implement the factory as fixture pattern to create dependencies between test fixtures

You’re now able to implement and maintain a solid test suite that will help you produce better and more reliable code, faster!