Exercice

Effectué

Dans cet exercice, vous allez utiliser pytest avec @pytest.mark.parametrize pour tester une fonction, y compris les cas qui attendent une exception. Vous allez ensuite restructurer un test fondé sur une classe en remplaçant l’approche de type xUnit setup_method() et teardown_method() par un fixture. L’utilisation de paramètres et de montages vous permet d’écrire des tests qui couvrent davantage de cas avec moins de répétition et de configuration plus claire.

Pour obtenir de l’aide sur la configuration, consultez la documentation Python pour les environnements virtual, la documentation pip pour pip install et la documentation pytest pour installation de pytest. Pour plus d’informations sur les fonctionnalités de pytest dans cet exercice, consultez la documentation de pytest pour paramétrer les fonctions de test, les assertions sur les exceptions attendues, les fixtures, la configuration de type xUnit et les répertoires et fichiers temporaires.

Avant de commencer

Utilisez Python 3.10 ou version ultérieure, comme décrit dans les prérequis du module. Si vous disposez déjà d’un environnement virtuel avec pytest installé et activé, passez à l’étape 1. Sinon, créez et activez un environnement virtuel dans le dossier dans lequel vous allez créer test_advanced.py.

Sur Windows PowerShell :

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version

Sur macOS, Linux ou Sous-système Windows pour Linux :

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version

La commande de version doit imprimer une version pytest, similaire à cette sortie :

pytest x.y.z

Les commandes d’installation n’épinglent pas pytest à une version spécifique. Pip utilise des métadonnées de package pour choisir une version pytest stable compatible avec la version Python dans l’environnement virtuel.

Utilisez python -m pytest tout au long de cet exercice afin que pytest s’exécute à partir de l’environnement actif. Si Windows PowerShell bloque Activate.ps1, consultez la documentation Python venv pour l'option de stratégie d'exécution PowerShell, ou exécutez la Python de l'environnement virtuel directement pour les commandes pip et pytest. Par exemple:

.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py

Étape 1 - Ajouter un fichier avec des tests pour cet exercice

  1. Dans le même dossier dans lequel vous allez exécuter pytest, créez un fichier de test nommé test_advanced.py. Ne placez pas le fichier dans le dossier .venv . Ajoutez le code suivant :

    import pytest
    
    
    def str_to_bool(string):
        normalized = string.lower()
        if normalized in ["yes", "y", "1"]:
            return True
        if normalized in ["no", "n", "0"]:
            return False
        raise ValueError(f"Cannot convert {string!r} to a boolean")
    

    La fonction str_to_bool() accepte une chaîne comme entrée. Elle retourne True pour les valeurs vraies reconnues, retourne False pour les valeurs fausses reconnues et lève une ValueError pour toute autre valeur.

  2. Dans le même fichier, ajoutez les tests pour la fonction str_to_bool(). Utilisez-la @pytest.mark.parametrize pour tester les valeurs vraies en premier :

    @pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
    def test_str_to_bool_true(string):
        assert str_to_bool(string) is True
    
  3. Ensuite, ajoutez un autre test avec les valeurs false :

    @pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
    def test_str_to_bool_false(string):
        assert str_to_bool(string) is False
    

    Il existe désormais deux fonctions de test couvrant les entrées représentatives pour les valeurs de retour des deux True et False.

  4. Enfin, ajoutez un test paramétrable pour les valeurs que la fonction ne doit pas accepter. Utilisez pytest.raises() comme gestionnaire de contexte pour que le test réussisse uniquement si l'exception attendue est levée par str_to_bool().

    @pytest.mark.parametrize("string", ["maybe", "2"])
    def test_str_to_bool_invalid(string):
        with pytest.raises(ValueError, match="Cannot convert"):
            str_to_bool(string)
    

    L’argument match vérifie le message d’exception avec une expression régulière. Ici, le test vérifie uniquement la partie stable du message afin qu’elle ne dépende pas de la valeur exacte non valide.

Remarque

Par souci de simplicité, cet exercice conserve le code sous test et les tests dans le même fichier. Dans les projets de Python réels, le code et les tests d’application sont généralement séparés en différents fichiers et répertoires, tels qu’un package src/ et un répertoire tests/.

Étape 2 - Exécuter les tests et explorer le rapport

Après avoir ajouté les tests, exécutez pytest et inspectez la sortie. Utilisez l’indicateur de détail accru (-v) afin que vous puissiez voir chaque valeur d’entrée traitée comme un test distinct.

Sur Windows PowerShell :

python -m pytest -v test_advanced.py

Sur macOS, Linux ou Sous-système Windows pour Linux :

python -m pytest -v test_advanced.py

La sortie doit être similaire au rapport suivant :

============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items

test_advanced.py::test_str_to_bool_true[Y] PASSED                        [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED                        [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED                        [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED                      [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED                       [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED                       [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED                       [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED                      [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED                 [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED                     [100%]

============================== 10 passed in 0.01s ==============================

Votre système d’exploitation, version de Python, version de pytest, chemin d'exécution, répertoire racine, lignes de plug-in et de cache, mise en forme de progression de la collecte, retour à la ligne et temps d'exécution peuvent différer. Bien que vous n’avez écrit que trois fonctions de test, pytest a collecté 10 tests, car chaque valeur d’entrée paramétrée s’exécute comme son propre cas de test.

Étape 3 – Transformer la configuration et le nettoyage en un fixture

  1. Ajoutez un test basé sur une classe au fichier test_advanced.py . Ce test repose sur des méthodes de type xUnit setup_method() et teardown_method() afin de créer et supprimer un fichier pour chaque méthode de test :

    from pathlib import Path
    
    
    class TestFile:
    
        def setup_method(self):
            self.path = Path("done")
            self.path.write_text("1", encoding="utf-8")
    
        def teardown_method(self):
            self.path.unlink(missing_ok=True)
    
        def test_done_file(self):
            contents = self.path.read_text(encoding="utf-8")
            assert contents == "1"
    

    Ce test peut réussir, mais il écrit un nom de fichier fixe dans le répertoire de travail actuel et nécessite un code de nettoyage distinct. Si une exécution de test est interrompue ou si des tests s’exécutent simultanément, par exemple avec pytest-xdist ou se chevauchent des exécutions locales, cette approche peut laisser des fichiers obsolètes, remplacer un fichier existant nommé terminé ou provoquer l’impact des tests les uns sur les autres.

  2. Intégrez un fixture au niveau du module en utilisant le fixture intégré tmp_path pour générer un fichier temporaire. À l’étape suivante, vous allez mettre à jour la classe pour utiliser ce dispositif à la place des méthodes d’installation et de destruction. Pour la lisibilité, placez ce dispositif de niveau module après les tests paramétrés str_to_bool et avant la classe TestFile. Pytest identifie les fixtures définis au niveau du module indépendamment de leur position ; leur emplacement vise donc uniquement la lisibilité pour les lecteurs humains :

    @pytest.fixture
    def tmp_file(tmp_path):
        def write():
            path = tmp_path / "done"
            path.write_text("1", encoding="utf-8")
            return path
        return write
    

    La tmp_file() fixture utilise la fixture pytest tmp_path, qui fournit un répertoire temporaire unique pathlib.Path pour chaque invocation de fonction de test, y compris chaque cas paramétré. Pytest gère le répertoire temporaire. Le test ne dépend donc pas d’un chemin d’accès de fichier codé en dur.

  3. Supprimez la classe antérieureTestFile, incluant ses méthodes setup_method() et teardown_method(), puis remplacez-la par cette version finale qui utilise le dispositif plutôt que les méthodes d'installation et de démontage :

    class TestFile:
    
        def test_done_file(self, tmp_file):
            path = tmp_file()
            contents = path.read_text(encoding="utf-8")
            assert contents == "1"
    

    Vous pouvez également supprimer l’importation from pathlib import Path que vous avez ajoutée à l’étape précédente, car le test ne crée plus d’objet Path directement.

Vérifier votre travail

À présent, vous devez disposer d’un fichier Python nommé test_advanced.py qui contient :

  • Fonction str_to_bool() qui accepte une chaîne et retourne une valeur booléenne pour les valeurs true et false reconnues.
  • Trois tests paramétrés pour la str_to_bool() fonction : un qui teste les valeurs de True, un qui teste les valeurs de False, et un autre qui vérifie que les valeurs non valides déclenchent ValueError.
  • Une fixture pytest personnalisée qui utilise la fixture intégrée tmp_path pour créer un fichier terminé temporaire avec un certain contenu.
  • Classe unique TestFile avec une méthode qui utilise le dispositif personnalisé tmp_file pour créer et lire le fichier.
  • Aucune from pathlib import Path importation ni aucune setup_method()/teardown_method() méthode dans la version finale.

Le fichier final doit ressembler à ce qui suit :

import pytest


def str_to_bool(string):
    normalized = string.lower()
    if normalized in ["yes", "y", "1"]:
        return True
    if normalized in ["no", "n", "0"]:
        return False
    raise ValueError(f"Cannot convert {string!r} to a boolean")


@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
    assert str_to_bool(string) is True


@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
    assert str_to_bool(string) is False


@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
    with pytest.raises(ValueError, match="Cannot convert"):
        str_to_bool(string)


@pytest.fixture
def tmp_file(tmp_path):
    def write():
        path = tmp_path / "done"
        path.write_text("1", encoding="utf-8")
        return path
    return write


class TestFile:

    def test_done_file(self, tmp_file):
        path = tmp_file()
        contents = path.read_text(encoding="utf-8")
        assert contents == "1"

Réexécutez le fichier de test.

Sur Windows PowerShell :

python -m pytest -v test_advanced.py

Sur macOS, Linux ou Sous-système Windows pour Linux :

python -m pytest -v test_advanced.py

Le rapport final doit afficher 11 tests collectés : 10 cas des tests paramétrés et un test basé sur une classe.

============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items

test_advanced.py::test_str_to_bool_true[Y] PASSED                        [  9%]
test_advanced.py::test_str_to_bool_true[y] PASSED                        [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED                        [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED                      [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED                       [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED                       [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED                       [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED                      [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED                 [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED                     [ 90%]
test_advanced.py::TestFile::test_done_file PASSED                        [100%]

============================== 11 passed in 0.01s ==============================

Tous les tests doivent réussir sans erreur.