Classes et méthodes de test

Effectué

En plus d’écrire des fonctions de test, Pytest vous permet d’utiliser des classes. Comme nous l’avons déjà mentionné, il n’est pas nécessaire d’hériter et les classes de test suivent quelques règles simples. L’utilisation de classes vous offre plus de flexibilité et de réutilisabilité. Comme vous le voyez ensuite, Pytest garde hors de la voie et évite de vous forcer à écrire des tests d’une certaine manière.

Tout comme les fonctions, vous pouvez toujours écrire des assertions à l’aide de l’instruction assert.

Créer une classe de test

Utilisons un scénario réel pour voir comment les classes de test peuvent aider. La fonction suivante vérifie si un fichier donné contient « oui » dans son contenu. Si c’est le cas, elle retourne True. Si le fichier n’existe pas ou s’il contient « non » dans son contenu, il retourne False. Ce scénario est courant dans les tâches asynchrones qui utilisent le système de fichiers pour indiquer la complétion.

Voici l’apparence de la fonction :

import os

def is_done(path):
    if not os.path.exists(path):
        return False
    with open(path) as _f:
        contents = _f.read()
    if "yes" in contents.lower():
        return True
    elif "no" in contents.lower():
        return False

Maintenant, voici comment une classe avec deux tests (un pour chaque condition) dans un fichier nommé test_files.py ressemble :

class TestIsDone:

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Attention

Les méthodes de test utilisent le chemin /tmp pour un fichier de test temporaire parce qu’il est plus facile à utiliser pour l’exemple. Toutefois, si vous avez besoin d’utiliser des fichiers temporaires, envisagez d’utiliser une bibliothèque telle que tempfile qui peut les créer (et les supprimer) sans problème pour vous. Les systèmes n’ont pas tous un répertoire /tmp et cet emplacement risque de ne pas être temporaire en fonction du système d’exploitation.

L’exécution des tests avec l’indicateur -v pour augmenter le niveau de détail montre les tests réussis :

pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
cachedir: .pytest_cache
rootdir: /private/
collected 2 items

test_files.py::TestIsDone::test_yes PASSED                               [ 50%]
test_files.py::TestIsDone::test_no PASSED                                [100%]

============================== 2 passed in 0.00s ===============================

Bien que les tests passent, ils semblent répétitifs et ils quittent également des fichiers une fois le test terminé. Avant de voir comment nous pouvons les améliorer, nous allons aborder les méthodes d’assistance dans la section suivante.

Méthodes d’assistance

Dans une classe de test, vous pouvez utiliser quelques méthodes pour configurer et supprimer l’exécution des tests. Pytest les exécute automatiquement s’ils sont définis. Pour utiliser ces méthodes, vous devez savoir qu’elles ont un ordre et un comportement spécifiques.

  • setup : s’exécute une fois avant chaque test d’une classe
  • teardown : s’exécute une fois après chaque test d’une classe
  • setup_class : s’exécute une fois avant tous les tests d’une classe
  • teardown_class : s’exécute une fois après tous les tests d’une classe

Lorsque les tests nécessitent des ressources similaires (ou identiques) pour fonctionner, il est utile d’écrire des méthodes d’installation. Dans l’idéal, un test ne doit pas laisser les ressources une fois qu’il est terminé, donc les méthodes de suppression peuvent aider à nettoyer les tests dans ces cas-là.

Nettoyage

Examinons une classe de test mise à jour qui nettoie les fichiers après chaque test :

class TestIsDone:

    def teardown(self):
        if os.path.exists("/tmp/test_file"):
            os.remove("/tmp/test_file")

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Comme nous avons utilisé la méthode teardown(), cette classe de test ne laisse plus de /tmp/test_file derrière.

Paramétrage

Une autre amélioration que nous pouvons apporter à cette classe consiste à ajouter une variable qui pointe vers le fichier. Étant donné que le fichier est maintenant déclaré à six emplacements, toutes les modifications apportées au chemin signifient qu’il faut les apporter à tous ces emplacements. Cet exemple montre comment la classe ressemble à une méthode setup() ajoutée qui déclare la variable de chemin d’accès :

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def test_yes(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("no")
        assert is_done(self.tmp_file) is False

Méthodes d’assistance personnalisées

Vous pouvez créer des méthodes d’assistance personnalisées dans une classe. Ces méthodes ne doivent pas être précédées du nom test et ne peuvent pas être nommées comme méthodes d’installation ou de nettoyage. Dans la classe TestIsDone, nous pouvons automatiser la création du fichier temporaire dans une assistance personnalisée. Cette méthode d’assistance personnalisée peut ressembler à cet exemple :

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

Pytest n’exécute pas automatiquement la méthode write_tmp_file() , et d’autres méthodes peuvent l’appeler directement pour enregistrer des tâches répétitives telles que l’écriture dans un fichier.

L’ensemble de la classe ressemble à cet exemple, après avoir mis à jour les méthodes de test pour utiliser l’assistance personnalisée :

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

    def test_yes(self):
        self.write_tmp_file("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        self.write_tmp_file("no")
        assert is_done(self.tmp_file) is False

Quand utiliser une classe au lieu d’une fonction

Il n’existe aucune règle stricte permettant de savoir quand utiliser une classe au lieu d’une fonction. Il est toujours judicieux de suivre les conventions dans les projets et les équipes actuels que vous utilisez. Voici quelques questions générales à poser qui peuvent vous aider à déterminer quand utiliser une classe :

  • Vos tests ont-ils besoin d’un code d’assistance de configuration ou de nettoyage similaire ?
  • Le regroupement de vos tests est-il logique ?
  • Y a-t-il au moins quelques tests dans votre suite de tests ?
  • Vos tests peuvent-ils tirer parti d’un ensemble commun de fonctions d’assistance ?