Exercice
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
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 retourneTruepour les valeurs vraies reconnues, retourneFalsepour les valeurs fausses reconnues et lève uneValueErrorpour toute autre valeur.Dans le même fichier, ajoutez les tests pour la fonction
str_to_bool(). Utilisez-la@pytest.mark.parametrizepour 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 TrueEnsuite, 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 FalseIl existe désormais deux fonctions de test couvrant les entrées représentatives pour les valeurs de retour des deux
TrueetFalse.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 parstr_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
matchvé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
Ajoutez un test basé sur une classe au fichier test_advanced.py . Ce test repose sur des méthodes de type xUnit
setup_method()etteardown_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-xdistou 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.Intégrez un fixture au niveau du module en utilisant le fixture intégré
tmp_pathpour 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ésstr_to_boolet avant la classeTestFile. 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 writeLa
tmp_file()fixture utilise la fixture pytesttmp_path, qui fournit un répertoire temporaire uniquepathlib.Pathpour 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.Supprimez la classe antérieure
TestFile, incluant ses méthodessetup_method()etteardown_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 Pathque vous avez ajoutée à l’étape précédente, car le test ne crée plus d’objetPathdirectement.
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 deTrue, un qui teste les valeurs deFalse, et un autre qui vérifie que les valeurs non valides déclenchentValueError. - Une fixture pytest personnalisée qui utilise la fixture intégrée
tmp_pathpour créer un fichier terminé temporaire avec un certain contenu. - Classe unique
TestFileavec une méthode qui utilise le dispositif personnalisétmp_filepour créer et lire le fichier. - Aucune
from pathlib import Pathimportation ni aucunesetup_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.