Voici une question qui sépare les amateurs des professionnels : comment savez-vous que votre code fonctionne ? Jusqu'à présent, votre réponse a été de l'exécuter et de regarder, ce qui est honnête mais ne passe pas à l'échelle. Modifiez une fonction dans un projet de mille lignes et le simple fait de regarder ne vous dira pas laquelle des quarante autres fonctions vous venez de casser. Les tests automatisés répondent définitivement à la question : de petits programmes qui vérifient vos programmes, s'exécutent en quelques secondes, après chaque modification, pour toujours. Cette leçon enseigne pytest, l'outil sur lequel le monde Python s'est standardisé, ainsi que les habitudes qui font des tests un accélérateur plutôt qu'une corvée.
Un changement de perspective avant la syntaxe, car l'attitude détermine si les tests s'ancrent dans la durée. Les tests ne sont pas de la bureaucratie ajoutée après le vrai travail ; ils sont le moyen d'aller plus vite. Avec une suite de tests, vous remaniez sans crainte, mettez à jour les dépendances avec désinvolture et acceptez votre propre code vieux de six mois sans le relire, car la suite se porte garante pour lui. Chaque leçon de ce cours vous y a préparé en douceur : les fonctions pures de la Partie 4 qui associent des entrées à des sorties sont précisément le type de code que les tests adorent. Aujourd'hui, cette préparation porte ses fruits.
Ce que vous apprendrez dans la Partie 13
- assert : l'unique instruction au cœur de tous les tests
- Conventions de pytest : fichiers, fonctions et zéro code standard superflu
- Lire la sortie d'échec, là où pytest gagne sa renommée
- Couvrir les cas limites avec parametrize
- Tester les exceptions avec raises, et les fichiers avec tmp_path
- Le rythme rouge-vert-refactoriser que les professionnels utilisent réellement
Remarque
Avant de commencer
Vous avez besoin des fonctions de la Partie 4 et des exceptions de la Partie 7. pytest lui-même ne peut pas s'exécuter dans le bac à sable du navigateur, donc cette leçon associe le flux de travail réel en terminal avec un bac à sable qui imite un exécuteur de tests, et chaque exemple se transfère tel quel sur votre machine.
1. assert : l'atome du test
Python possède une instruction conçue pour les affirmations : assert prend une expression, ne fait rien quand elle est évaluée comme vraie, et lève une AssertionError dans le cas contraire. Chaque test que vous écrirez se réduit à des assert : appelez votre fonction avec une entrée connue, vérifiez avec assert que la sortie correspond à ce que vous attendez. C'est toute la théorie des tests unitaires, en une phrase. Tout ce que pytest ajoute, c'est de l'organisation, de la découverte et un rapport d'échec spectaculaire autour de cette unique instruction.
def grade_for(score: int) -> str:
if score >= 75:
return "A"
elif score >= 65:
return "B"
elif score >= 50:
return "C"
return "F"
assert grade_for(80) == "A"
assert grade_for(75) == "A" # boundary: exactly 75
assert grade_for(74) == "B" # boundary: just below
assert grade_for(0) == "F"
print("all claims hold")
Regardez quelles entrées ces assert ont choisies : pas des scores aléatoires mais les frontières, exactement 75, juste en dessous de 75, et l'extrême. Les bogues se cachent aux limites, aux seuils de décalage d'une unité, à la liste vide, au zéro, au None, et choisir des entrées qui explorent les frontières est la véritable compétence du test. La mécanique s'apprend en un après-midi ; l'instinct de là où le code casse grandit avec chaque bogue rencontré, et les tests sont la manière dont vous mettez chacun en bouteille pour qu'il ne morde jamais deux fois.
2. pytest : des conventions plutôt que des cérémonies
pytest, installé avec pip install pytest, transforme les assert en une suite gérée grâce à trois conventions : les fichiers de test sont nommés test_quelquechose.py, les fonctions de test commencent par test_, et le corps est constitué de simples assert, sans classes, sans enregistrement, sans code standard superflu. Lancez pytest dans le dossier de votre projet et il découvre chaque test, les exécute tous et fait un rapport. Un point vert par réussite, un rapport détaillé par échec, et ce rapport est la raison pour laquelle pytest a gagné : il réévalue l'expression en échec et montre les valeurs réelles de chaque côté, de sorte que la plupart des échecs sont diagnostiqués avant que votre café ne refroidisse.
# test_grades.py
from grades import grade_for
def test_top_grade():
assert grade_for(80) == "A"
def test_boundary_75_is_a():
assert grade_for(75) == "A"
def test_failing_score():
assert grade_for(20) == "F"
$ pytest
=========== test session starts ===========
collected 3 items
test_grades.py ..F [100%]
================ FAILURES =================
______________ test_failing_score _________
def test_failing_score():
> assert grade_for(20) == "F"
E AssertionError: assert 'S' == 'F'
E (diff shown by pytest)
========= 1 failed, 2 passed in 0.04s =====
Lisez ce bloc d'échec une fois et vous savez tout : quel test, quelle ligne, et le fait crucial que grade_for(20) a retourné "S" alors que le test attendait "F", ce qui signifie que quelqu'un a ajouté une note S à l'échelle et que le test a instantanément détecté le changement de comportement. C'est le contrat que les suites font respecter : les changements de comportement se manifestent bruyamment au lieu de passer inaperçus, et l'équipe décide si c'est le code ou l'attente qui est incorrect. Les deux réponses sont acceptables ; ne pas savoir est ce que les suites abolissent.
Point de contrôle
Qu'est-ce qui rend une fonction facile à tester ?
3. parametrize : un test, de nombreux cas
L'échelle des notes nécessite huit ou neuf vérifications aux limites, et copier une fonction de test neuf fois est exactement la duplication que la Partie 4 vous a appris à refuser. Le décorateur parametrize de pytest exécute un corps de test sur une table de cas, chacun rapporté individuellement, de sorte qu'un seul échec nomme son entrée exacte. Le test se lit comme une spécification de la fonction, ce qui est l'idéal discret de toute cette discipline : les tests comme documentation exécutable de ce que le code promet.
import pytest
from grades import grade_for
@pytest.mark.parametrize("score, expected", [
(100, "A"), (75, "A"), # top band and its floor
(74, "B"), (65, "B"), # next band, both edges
(64, "C"), (50, "C"),
(49, "F"), (0, "F"),
])
def test_grade_bands(score, expected):
assert grade_for(score) == expected
Deux autres outils complètent la trousse quotidienne. Quand une fonction doit lever une exception, vérifier le chemin heureux ne suffit pas ; pytest.raises valide le contrat d'échec vu dans la Partie 7 : with pytest.raises(ValueError): withdraw(100, -5). Et quand le code touche aux fichiers, la fixture tmp_path fournit un dossier temporaire neuf automatiquement nettoyé, ce qui rend testables les compétences de manipulation de fichiers de la Partie 7 sans encombrer votre disque. Les fixtures vont bien plus loin (configuration partagée, bases de données, faux serveurs), mais raises et tmp_path couvrent honnêtement la première année d'un débutant.
import pytest
from billing import withdraw, load_scores
def test_negative_amount_rejected():
with pytest.raises(ValueError):
withdraw(balance=100, amount=-5)
def test_load_scores_skips_bad_lines(tmp_path):
f = tmp_path / "grades.txt"
f.write_text("Amina,87\nbroken\nZane,91\n")
scores = load_scores(f)
assert scores == [("Amina", 87), ("Zane", 91)]
Le mot fixture mérite une définition précise puisque vous venez d'en utiliser une. Une fixture est un élément de configuration nommé que pytest construit et injecte quand un test la demande par son nom de paramètre, exactement comme tmp_path est apparu sans import. Vous pouvez définir les vôtres avec le décorateur @pytest.fixture (un inventaire type, un carnet de notes peuplé, une configuration analysée) et tout test qui la nomme reçoit une copie neuve. C'est ainsi que les suites partagent la configuration sans partager l'état. Rangez le mécanisme complet dans la case « à apprendre quand nécessaire » ; reconnaître le patron d'injection est ce qui compte aujourd'hui.
Quand les suites grossissent, un peu d'organisation les garde agréables. La convention est un dossier tests à côté du code, un fichier de test par module (test_parsing.py qui teste parsing.py), pour que la suite reflète la base de code et que personne ne cherche rien. L'option -k de pytest exécute un sous-ensemble par correspondance de nom (pytest -k slug pendant que vous travaillez sur slugify), et -x s'arrête au premier échec quand vous voulez corriger une chose à la fois. Plus tard, un outil de couverture montrera quelles lignes aucun test ne touche ; considérez ce chiffre comme une lampe torche pour les coins oubliés, pas comme un score à maximiser, car une assertion sans signification peut gonfler la couverture sans rien protéger.
4. Le rythme : rouge, vert, refactoriser
Outils en main, voici comment les professionnels avancent réellement, une boucle en trois temps. Rouge : écrivez un petit test qui échoue et qui fige le prochain comportement souhaité. Vert : écrivez le code le plus simple qui le fait passer. Refactoriser : nettoyez avec la suite qui monte la garde. La discipline de commencer par le rouge compte plus qu'il n'y paraît, car un test que vous n'avez jamais vu échouer ne prouve rien ; le voir échouer d'abord prouve qu'il le peut. Parcourez un cycle complet ci-dessous, le même rythme de démonstration que le stepper de paquet de la Partie 8.
Un cycle rouge-vert-refactoriser, temps par temps
Nous voulons que slugify() transforme des titres en slugs d'URL. Aucun code n'existe encore, donc cela échoue immédiatement, et cet échec est la spécification.
def test_slugify_basic():
assert slugify("Learn Python!") == "learn-python"
$ pytest -q
F NameError: name 'slugify' is not defined
Mettre en minuscules, supprimer les caractères non alphanumériques avec les regex de la Partie 11, joindre par des tirets. Pas d'astuce ; passer est le seul objectif de ce temps.
import re
def slugify(title: str) -> str:
words = re.findall(r"[a-z0-9]+", title.lower())
return "-".join(words)
$ pytest -q
. 1 passed
Qu'en est-il des espaces supplémentaires et des tirets Unicode ? Ajoutez le cas à une table paramétrée et regardez-le échouer ou passer honnêtement.
@pytest.mark.parametrize("title, slug", [
("Learn Python!", "learn-python"),
(" Hello World ", "hello-world"),
("A/B Testing 101", "a-b-testing-101"),
])
def test_slugify(title, slug):
assert slugify(title) == slug
L'approche par regex gère déjà ces cas. Quand un cas échoue à la place, vous corrigez la fonction, pas le test, sauf si l'attente elle-même était fausse.
$ pytest -q ... 3 passed in 0.03s
Renommez, extrayez, simplifiez, réexécutez. La suite rend le refactoring sûr, et un refactoring sûr garde la base de code jeune. Ce temps est la récompense des deux autres.
$ pytest -q # after every small cleanup ... 3 passed
Point de contrôle
Pourquoi devriez-vous regarder un nouveau test échouer avant de le faire passer ?
5. Pratique : un exécuteur de tests dans le terrain de jeu
pytest lui-même a besoin d'un vrai terminal, mais son essence (découvrir les fonctions nommées test_*, les exécuter, rapporter les assertions) tient en vingt lignes du Python que vous connaissez maintenant, réflexion incluse. Le terrain de jeu ci-dessous contient exactement cela : un micro-exécuteur, une fonction à tester avec un bogue planté, et une petite suite. Trouvez le bogue en lisant les échecs, corrigez-le, et passez au vert, la boucle professionnelle complète en miniature. Ensuite, sur votre machine, pip install pytest et exécutez le vrai sur l'exemple slugify ci-dessus.
L'exercice 2 introduit en douce l'idée la plus profonde de la leçon : le test a imposé une décision de conception, à savoir si la ponctuation fait partie des mots, et c'est parfaitement normal. Les tests interrogent vos intentions pendant que le code est encore malléable. Les équipes découvrent l'essentiel de leur spécification de cette manière, un cas de test délicat après l'autre, bien avant que les utilisateurs ne le fassent à leur place en production.
! Erreurs fréquentes à éviter
-
✕Ne tester que le chemin nominal avec des entrées bienveillantes.
✓Les bugs se nichent dans les cas limites : entrée vide, zéro, un seul élément, frontières, données mal formées. Un test de cas limite vaut cinq tests nominaux ; la table parametrize rend les cas limites peu coûteux.
-
✕Écrire des tests qui dépendent les uns des autres ou de l'ordre d'exécution.
✓Chaque test construit son propre monde (tmp_path, objets frais) et vérifie de manière indépendante. Les suites dépendantes de l'ordre pourrissent vite et échouent de façon mystérieuse.
-
✕Vérifier des blocs énormes, comme une chaîne de rapport complète.
✓Vérifiez les faits significatifs : compteurs, totaux, présence de lignes clés. Les assertions sur des blocs entiers cassent à chaque modification cosmétique et apprennent à l'équipe à ignorer les échecs.
-
✕Sauter l'étape du test qui échoue d'abord et faire confiance au vert.
✓Cassez délibérément le code une fois et confirmez que le test passe au rouge. Dix secondes de paranoïa valident l'ensemble du filet de sécurité.
? Questions fréquentes
Quelle quantité de tests un débutant doit-il écrire ? +
Chaque fonction dont la logique mérite d'être conservée : analyse, calcul, décisions. Ignorez le code de liaison trivial. Le minimum honnête qui change votre vie est une poignée de tests de cas limites sur les fonctions que vous redoutez de modifier.
Qu'est-ce que le module unittest que je vois dans les anciens tutoriels ? +
Le framework de la bibliothèque standard qui a précédé pytest, basé sur des classes et plus cérémonieux. pytest exécute très bien les suites unittest, et pratiquement tous les nouveaux projets Python choisissent pytest ; ce cours aussi.
Dois-je écrire les tests avant ou après le code ? +
Le strict test-first est une école ; la réponse honnête de l'industrie est les deux, de manière fluide. Ce qui n'est pas négociable, c'est que le test existe, que vous l'ayez vu échouer au moins une fois et qu'il s'exécute à chaque modification.
Comment les tests s'exécutent-ils automatiquement à chaque modification ? +
Intégration continue : des services exécutent votre suite à chaque push et bloquent les fusions en cas d'échec. Lorsque vous aborderez notre série avancée, la leçon sur les tests y connectera pytest exactement à ce pipeline pour une véritable API web.
6. Récapitulatif et perspectives
Vous disposez désormais du filet de sécurité du professionnel : assert comme atome, les conventions de pytest et ses rapports d'échec lumineux, parametrize pour les tables de cas limites, raises pour les contrats d'erreur, tmp_path pour le travail sur fichiers, et le rythme rouge-vert-refactoriser qui transforme tout cela en un style de travail. Combiné avec les annotations de type de la Partie 10, votre code déclare maintenant ses contrats et les prouve, ce qui est, en une phrase, ce que signifie le Python professionnel. Lorsque vous testerez plus tard des services web, la leçon sur les tests avec FastAPI de notre série production prolonge directement celle d'aujourd'hui.
Et avec cela, le parcours fondamental est terminé. Les trois dernières leçons sont la destination vers laquelle tout le programme a cheminé : Partie 14, fondements du machine learning avec scikit-learn, où les compétences en données des Parties 5, 9 et 11 entraînent leur premier vrai modèle. La leçon sur les tests dans l'application Learn Python ci-dessous reflète le contenu d'aujourd'hui, et le programme complet se trouve sur le hub de la série.
Conseil de pro
Adoptez dès aujourd'hui le réflexe bug-to-test : chaque fois que vous corrigez un bug, écrivez le test qui l'aurait détecté, puis regardez-le réussir. Votre suite devient un musée de toutes les erreurs que vous avez jamais commises, ce qui est précisément la raison pour laquelle vous ne les commettrez plus jamais.
Pratiquez en mobilité
Learn Python, l'application Android gratuite
Chaque sujet de cette série se retrouve aussi dans l'application : leçons concises, exemples exécutables, quiz, mini-projets et un environnement Python hors ligne qui fonctionne sur votre téléphone.
Commentaires
0Aucun commentaire pour l’instant. Soyez la première personne à donner votre avis.