Tests unitaires en Python
Un test unitaire vérifie un comportement utile dans un contexte contrôlé. L'« unité » est une frontière de conception, pas nécessairement une fonction : testez la plus petite surface qui exprime le contrat sans reconstruire son implémentation dans le test.
import pytest
def normalize_name(raw: str) -> str:
name = raw.strip().casefold()
if not name:
raise ValueError("empty name")
return name
@pytest.mark.parametrize(
("raw", "expected"),
[(" Ada ", "ada"), ("GRACE", "grace")],
)
def test_normalize_name(raw: str, expected: str) -> None:
assert normalize_name(raw) == expected
def test_normalize_name_rejects_empty_text() -> None:
with pytest.raises(ValueError, match="empty"):
normalize_name(" ")
Python fournit unittest ; pytest est un exécuteur tiers avec assertions concises, fixtures et paramétrage. Les deux conviennent. Préférez une convention cohérente au mélange sans raison.
Forme d'un test utile
- Préparez uniquement l'état pertinent pour le scénario.
- Agissez par une frontière publique ou volontairement stable.
- Vérifiez le résultat observable, le changement d'état, l'événement émis ou l'échec.
- Nommez le test selon le comportement et la condition.
Le paramétrage est utile lorsque plusieurs entrées partagent un contrat. Séparez les cas lorsque préparation, comportement attendu ou diagnostic diffèrent. Les valeurs des paramètres peuvent être mutables et ne sont pas automatiquement copiées entre cas pytest.
Fixtures et isolation
Les fixtures possèdent la préparation et le nettoyage des ressources réutilisables. Gardez leur portée aussi étroite que possible ; un état mutable partagé à l'échelle de la session couple les tests et rend les échecs dépendants de l'ordre. Utilisez tmp_path plutôt que d'écrire dans le dépôt ou la configuration réelle de l'utilisateur.
Un test doit contrôler temps, hasard, variables d'environnement et E/S externes s'ils rendent le résultat non déterministe. Une graine fixe est reproductible, mais ne couvre que les exemples générés, sauf variation volontaire des graines.
Doublures de test
Faux, bouchons, espions et simulacres remplacent des collaborateurs pour des raisons différentes. Remplacez la référence là où le nom est recherché, pas là où sa définition d'origine se trouve. Préférez une petite implémentation factice ou un objet appelable injecté si le contrat devient plus clair.
Un excès de simulacres ne fait que confirmer une séquence d'appels privée. Conservez un vrai test d'intégration pour les frontières importantes : bases de données, clients HTTP, systèmes de fichiers, sérialisation et sous-processus.
Assertions durables
Vérifiez des faits sémantiques, pas un ordre, format, horodatage ou instantané complet accidentel, sauf contrat public. Vérifiez résultats et effets de bord importants. Pour les échecs, contrôlez type d'exception et détails significatifs sans figer tout un message instable.
Le test doit pouvoir échouer pour la régression visée. Lors d'une correction, observez si possible le nouveau test échouer face au comportement cassé avant d'accepter la solution.
Exécuter l'exemple
Enregistrez le premier bloc dans test_names.py. Dans un environnement où pytest est déjà installé, lancez python -m pytest -q test_names.py. Il vérifie deux normalisations et un rejet. Dans un vrai projet, importez normalize_name depuis son module plutôt que de dupliquer l'implémentation dans le test. Le contrat retire les blancs aux extrémités et replie la casse ; il ne valide pas complètement les noms humains.
Sans pytest, utilisez unittest, fourni avec Python :
import unittest
class NameTests(unittest.TestCase):
def test_normalization(self):
self.assertEqual(normalize_name(" Ada "), "ada")
def test_empty(self):
with self.assertRaises(ValueError):
normalize_name(" ")
Placez cette classe et la définition de la fonction dans test_names.py, sans import ni tests pytest, puis lancez python -m unittest -v test_names. La découverte repère les méthodes test* des sous-classes de TestCase ; définir des tests ne les exécute pas.