Tester du code Python avec pytest

Écrivez une vraie suite de tests en Python : un assert simple, un test qui échoue en vous disant exactement ce qui a cassé, parametrize pour de nombreux cas, pytest.raises pour le cas d’erreur, et une fixture pour la configuration partagée — le tout avec une seule commande pytest.

Passez de l’inspection visuelle de la sortie à une vraie suite de tests. Écrivez des tests en Python avec un assert simple, exécutez-les avec pytest, et lisez l’introspection des assertions d’un test qui échoue — pytest affiche la valeur réelle, pas seulement « failed » — puis couvrez de nombreux cas avec parametrize, vérifiez le cas d’erreur avec pytest.raises et partagez la configuration avec une fixture. Copiez chaque exemple dans un fichier de test et lancez pytest dans votre terminal.

Date de publication

18 juillet 2026

Modifié

18 juillet 2026

AstucePoints clés
  • Un test n’est qu’une fonction qui vérifie votre code — pytest les exécute tous avec une seule commande. Un test est une petite fonction qui affirme que votre code fait ce que vous attendez ; pytest trouve chaque fonction test_* dans chaque fichier test_*.py et les exécute, si bien qu’un seul pytest vous dit ce qui fonctionne encore et ce qui a cassé.
  • Écrivez les assertions avec un simple assert — aucune méthode spéciale à mémoriser. Une assertion est une affirmation qui doit être vraie ; assert apply_discount(100, 25) == 75.0 constitue tout le test. C’est là l’avantage de pytest sur le module standard unittest : un assert lisible, pas self.assertEqual(...).
  • Un test qui échoue vous montre la valeur réelle — c’est là tout l’intérêt. L’introspection des assertions de pytest affiche assert 25.0 == 30.0 avec where 25.0 = apply_discount(50, 50), si bien que vous voyez ce que votre code a renvoyé face à ce que vous attendiez, sans ajouter le moindre print().
  • Un test, de nombreux cas avec @pytest.mark.parametrize ; vérifiez le cas d’erreur avec pytest.raises. Parametrize exécute le même test une fois par jeu d’entrées ; pytest.raises vérifie qu’une entrée invalide lève bien l’exception attendue.
  • Partagez la configuration avec une fixture ; c’est le jugement qu’une IA saute. Une fixture est une configuration réutilisable que pytest fournit à tout test qui la nomme. L’assistant écrit la fonction — vous écrivez le test qui la prouve, et relancez la suite après chaque changement.

Introduction

Vous changez une ligne — apply_discount arrondit désormais au centime — et vous livrez. Une semaine plus tard, la finance signale que les totaux de commande sont faux d’un centime sur des milliers d’enregistrements. Le changement paraissait anodin, rien n’a planté, et vous n’aviez aucun moyen de le remarquer : vous vérifiez le code comme la plupart des gens au début, en l’exécutant et en inspectant la sortie du regard. C’est exactement cette faille qu’un test referme. Un test est une petite fonction qui énonce ce que votre code devrait renvoyer et échoue bruyamment dès que ce n’est plus le cas — ainsi une régression (un changement qui casse quelque chose qui fonctionnait) est détectée à l’instant où vous l’introduisez, pas une semaine plus tard dans un rapport financier.

pytest est l’outil de test de fait pour Python. Vous écrivez des fonctions simples avec de simples instructions assert, vous exécutez une seule commande, et pytest rapporte exactement ce qui a réussi, ce qui a échoué, et — quand quelque chose échoue — la valeur réelle que votre code a produite. Cette leçon construit une vraie suite dans l’ordre où vous l’utiliserez : votre premier test, son exécution, la lecture d’un échec (la partie qui justifie pytest), la couverture de nombreux cas d’un coup, la vérification du cas d’erreur, et le partage de la configuration avec une fixture.

Tout ce qui suit s’exécute dans votre terminal, pas dans le navigateur : copiez chaque fichier dans votre projet et lancez pytest dans votre propre shell. Installez-le une fois dans l’environnement virtuel de votre projet avec pip install pytest (voir Environnements virtuels et dépendances en Python).

Le code à tester

Tester nécessite quelque chose à tester. Voici une toute petite fonction qui applique une remise en pourcentage à un prix et l’arrondit au centime. Enregistrez-la sous discounts.py :

def apply_discount(price, pct):
    if not 0 <= pct <= 100:
        raise ValueError("pct must be between 0 and 100")
    return round(price * (1 - pct / 100), 2)

Deux comportements méritent d’être verrouillés : elle calcule le prix remisé (et l’arrondit), et elle rejette un pourcentage en dehors de 0–100 en levant ValueError. De bons tests couvriront les deux — le cas nominal et le cas d’erreur.

Votre premier test

Un test pytest est une fonction ordinaire dont le nom commence par test_, située dans un fichier dont le nom commence par test_, et contenant une assertion — une affirmation qui doit être vraie. Si l’expression assert est vraie, le test réussit ; si elle est fausse, le test échoue. Enregistrez ceci sous test_discounts.py, à côté de discounts.py :

from discounts import apply_discount

def test_applies_percentage():
    assert apply_discount(100, 25) == 75.0

def test_rounds_to_cents():
    assert apply_discount(19.99, 10) == 17.99

Pas de classe de base, pas de setUp, pas de méthodes d’assertion spéciales — importez simplement votre fonction et affirmez ce qu’elle doit renvoyer. Chaque fonction test_* est un test indépendant : le premier vérifie le calcul du pourcentage, le second vérifie l’arrondi. Ce style d’assert lisible est la différence majeure avec le unittest intégré à Python, où les mêmes vérifications s’écrivent self.assertEqual(...) à l’intérieur d’une sous-classe de TestCase (voir la documentation d’unittest pour le contraste). Le guide de démarrage complet se trouve dans le guide « Get Started » de pytest.

Exécutez-le : pytest

Depuis le dossier qui contient les deux fichiers, lancez pytest. L’option -q (« quiet ») garde la sortie courte :

pytest -q

pytest découvre votre fichier de test, exécute les deux fonctions de test, et rapporte :

....                                                                     [100%]
4 passed in 0.01s

Chaque point est un test réussi. Vous avez écrit deux fonctions mais vous voyez quatre points — les deux tests simples plus deux autres que nous allons ajouter avec parametrize (montrés ici pour que le compte corresponde au fichier final). La dernière ligne est le résumé : tout a réussi. (Le temps varie d’une exécution à l’autre — vous verrez quelque chose comme 0.01s, pas exactement ce nombre.) Voilà toute la boucle : changez le code, lancez pytest, obtenez un verdict instantané. La valeur apparaît quand un test échoue.

Lire un échec — le vrai intérêt

Supposons que vous écriviez un test avec la mauvaise attente exprès — vous pensez que la moitié de 50 devrait valoir 30 :

def test_half_off():
    assert apply_discount(50, 50) == 30.0   # wrong expectation on purpose

Lancez pytest et lisez ce qu’il affiche :

F                                                                        [100%]
=================================== FAILURES ===================================
________________________________ test_half_off _________________________________

    def test_half_off():
>       assert apply_discount(50, 50) == 30.0   # wrong expectation on purpose
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       assert 25.0 == 30.0
E        +  where 25.0 = apply_discount(50, 50)

test_buggy.py:4: AssertionError
=========================== short test summary info ============================
FAILED test_buggy.py::test_half_off - assert 25.0 == 30.0
1 failed in 0.01s

C’est l’introspection des assertions de pytest, et c’est la raison pour laquelle un simple assert suffit. Lisez les trois lignes clés :

  • La ligne > marque l’instruction exacte qui a échoué — l’assert que pytest était en train d’évaluer.
  • La ligne E assert 25.0 == 30.0 réécrit votre assertion avec les valeurs réelles substituées : votre code a renvoyé 25.0, vous attendiez 30.0. Une simple « AssertionError » vous dirait seulement qu’il a échoué ; pytest vous dit quelle était réellement la valeur.
  • La ligne E + where 25.0 = apply_discount(50, 50) montre d’où vient ce 25.0 — l’appel et ses arguments.

Vous obtenez la comparaison valeur-réelle-contre-valeur-attendue gratuitement, sans print() ni débogueur. Ici le test était faux (la moitié de 50 est bien 25.0) ; dans le travail réel, cette même sortie est ce qui vous pointe droit sur le bug. La mécanique des assertions et de cette introspection est documentée dans le guide pratique des assertions de pytest.

Couvrir de nombreux cas avec @pytest.mark.parametrize

Votre fonction rejette un pourcentage inférieur à 0 ou supérieur à 100. Vous pourriez écrire un test par mauvaise valeur, mais c’est du copier-coller. Parametrize — exécuter le même test une fois par jeu d’entrées — les fusionne en un seul :

import pytest
from discounts import apply_discount

@pytest.mark.parametrize("pct", [-5, 150])
def test_rejects_out_of_range(pct):
    with pytest.raises(ValueError):
        apply_discount(100, pct)

Le décorateur nomme un paramètre, pct, et lui donne une liste de valeurs. pytest exécute le corps du test une fois pour chacune — ici deux fois, avec pct=-5 puis pct=150 — et rapporte chacune comme un test distinct, si bien que si une seule valeur casse vous savez exactement laquelle. Ce sont les deux points supplémentaires de l’exécution 4 passed ci-dessus. Ajoutez une valeur à la liste et vous avez ajouté un cas de test sans ajouter de code. La syntaxe complète (plusieurs paramètres, ids, décorateurs empilés) est dans le guide pratique de parametrize.

Vérifier le cas d’erreur avec pytest.raises

Le test ci-dessus montre aussi l’autre moitié d’une bonne couverture : vérifier qu’une mauvaise entrée échoue comme elle le doit. pytest.raises est un gestionnaire de contexte qui ne passe que si le code à l’intérieur lève l’exception nommée — et fait échouer le test si le code ne lève pas :

def test_rejects_negative():
    with pytest.raises(ValueError):
        apply_discount(100, -5)

Sans cela, une fonction qui accepterait silencieusement -5 passerait inaperçue. Avec cela, « rejeter une entrée invalide » devient un comportement testé et garanti. Vous pouvez aussi vérifier le message avec match=. Voir la section sur les exceptions attendues du guide pratique des assertions.

Partager la configuration avec une fixture

Quand plusieurs tests ont besoin des mêmes données de départ, ne les reconstruisez pas dans chacun — utilisez une fixture : une configuration réutilisable que pytest crée et fournit à tout test qui la nomme comme argument. Définissez-la avec @pytest.fixture, en renvoyant ce dont les tests ont besoin :

import pytest
from discounts import apply_discount

@pytest.fixture
def cart():
    return [("book", 40.0), ("pen", 5.0)]   # (name, price) pairs

def test_totals_after_discount(cart):
    prices = [apply_discount(price, 10) for _name, price in cart]
    assert prices == [36.0, 4.5]

def test_cart_has_two_items(cart):
    assert len(cart) == 2

Les deux tests prennent un argument cart. pytest voit qu’une fixture nommée cart existe, l’appelle, et passe la valeur renvoyée — chaque test reçoit sa propre copie fraîche, si bien qu’un test ne peut pas corrompre les données d’un autre. La configuration vit à un seul endroit ; ajoutez un troisième test qui a besoin d’un panier et vous nommez simplement cart à nouveau. (Copiez ceci dans votre suite et lancez pytest — les deux tests passent aux côtés des autres.) Les fixtures s’étendent de cet exemple aux connexions de base de données et aux répertoires temporaires ; le guide pratique des fixtures couvre la portée, le nettoyage, et les fixtures intégrées comme tmp_path.

Comment pytest trouve vos tests

pytest exécute les tests qu’il peut trouver, et il les trouve par découverte des tests — en confrontant fichiers, classes et fonctions à des conventions de nommage. Suivez les noms et pytest récupère vos tests automatiquement ; cassez-les et pytest n’exécute silencieusement rien. Les conventions :

Ce que pytest recherche Règle de nommage Exemple
Fichiers de test nommés test_*.py ou *_test.py test_discounts.py
Fonctions de test préfixées par test_ def test_applies_percentage():
Classes de test préfixées par Test, et sans méthode __init__ class TestDiscount:
Méthodes de test préfixées par test_, dans une classe Test* def test_negative(self):

La plupart des suites utilisent simplement des fichiers test_*.py avec des fonctions test_* — les classes sont un regroupement optionnel. La liste faisant autorité est dans les conventions de pytest pour la découverte des tests. Si pytest rapporte « no tests ran », un faux pas de nommage en est presque toujours la cause (voir Problèmes fréquents).

Quand une IA écrit le code

Demandez à un assistant de code d’« écrire apply_discount » et vous obtiendrez une fonction plausible en quelques secondes — elle pourrait même arrondir correctement. Mais « ça a l’air correct » est exactement le piège que cette leçon existe pour refermer : une fonction générée est une réponse non vérifiée, et le jugement qu’une réponse en une invite saute, c’est de prouver qu’elle fait ce dont vous avez besoin sur les cas qui vous importent. Cette preuve, c’est le test. La compétence durable n’est pas de mémoriser la syntaxe de pytest — c’est de vous faire juge de toute réponse, qu’elle vienne d’une IA, d’un résultat de recherche ou d’un collègue : écrivez l’assertion, exécutez-la sur le vrai runtime, et lisez la valeur réelle en retour.

La répartition du travail est donc claire. Laissez l’assistant rédiger la fonction ; vous écrivez le test qui verrouille son comportement — le cas nominal, l’arrondi, l’entrée rejetée — et relancez la suite après chaque changement. Une suite qui passe est une preuve ; un extrait généré est une affirmation. Quand vous changerez cette ligne d’arrondi le mois prochain, le test restera vert ou vous dira, en une commande, exactement ce qui a cassé.

🟢 Avec un agent IA

Vous travaillez avec du code généré ? Collez la fonction et demandez à Prova « écris des tests pytest qui prouvent que ceci fonctionne — le cas nominal, les cas limites et le cas d’erreur » — puis exécutez-les vous-même et lisez la sortie. Prova rédige les tests ; c’est l’exécution qui passe (ou qui échoue) qui est votre preuve, pas sa parole. C’est tout l’intérêt d’un test : vous ne faites pas confiance à la réponse, vous la vérifiez. The runtime is the judge. Demander à Prova →

Problèmes fréquents

pytest dit « no tests ran ». Votre fichier ou votre fonction n’est pas nommé comme la découverte l’attend. Le fichier doit commencer par test_ (ou finir par _test.py) et chaque fonction de test doit commencer par test_check_discount() dans checks.py est invisible pour pytest. Renommez en test_discount() dans test_checks.py et il est collecté. C’est l’accroc le plus courant la première fois.

Un test passe alors qu’il ne devrait pas. Presque toujours un assert manquant — un test qui calcule une valeur mais n’affirme jamais rien n’a rien qui puisse échouer, donc pytest le marque comme réussi. apply_discount(100, 25) seul n’est pas un test ; assert apply_discount(100, 25) == 75.0 en est un. Chaque test a besoin d’au moins une vraie assertion.

ModuleNotFoundError: No module named 'discounts'. pytest ne peut pas importer votre code parce qu’il s’exécute depuis le mauvais endroit ou que l’agencement de votre projet cache le module. Lancez pytest depuis la racine de votre projet (le dossier contenant votre code), gardez le fichier de test à côté du code pour les projets simples, ou ajoutez un conftest.py vide à la racine — sa présence marque la racine et la met sur le chemin d’import. L’agencement de projet et les imports sont couverts dans les bonnes pratiques de pytest.

Questions fréquentes

Écrivez une fonction dont le nom commence par test_, dans un fichier dont le nom commence par test_, et mettez à l’intérieur un simple assert qui énonce ce que votre code doit faire — par exemple def test_applies_percentage(): assert apply_discount(100, 25) == 75.0. Puis lancez pytest depuis ce dossier. pytest trouve la fonction par son nom, l’exécute, et rapporte une réussite si l’assertion est vraie ou un échec détaillé (montrant la valeur réelle) si elle ne l’est pas. Aucune classe de base ni méthode d’assertion spéciale n’est requise.

pytest est le framework de test le plus utilisé pour Python. Vous écrivez les tests comme des fonctions ordinaires avec de simples instructions assert, et pytest les découvre et les exécute avec une seule commande, en rapportant ce qui a réussi et ce qui a échoué. Sa caractéristique remarquable est l’introspection des assertions : quand une assertion échoue, pytest affiche la valeur réelle que votre code a produite — pas seulement « AssertionError » — si bien que vous voyez exactement ce qui n’a pas fonctionné. Il ajoute aussi les fixtures pour la configuration partagée, @pytest.mark.parametrize pour exécuter un test sur de nombreuses entrées, et pytest.raises pour tester les cas d’erreur.

unittest est le framework de test intégré à Python, modelé sur JUnit de Java : vous sous-classez unittest.TestCase et utilisez des méthodes comme self.assertEqual(a, b) et self.assertRaises(...). pytest est un framework tiers (installez-le avec pip install pytest) qui vous laisse écrire les tests comme de simples fonctions avec de simples instructions assert, et donne une sortie d’échec plus riche. pytest peut aussi exécuter des tests de style unittest, donc l’adopter ne signifie pas réécrire les tests existants. Pour les nouveaux projets, la plupart des développeurs Python se tournent vers pytest pour sa syntaxe plus légère ; unittest convient quand vous voulez zéro dépendance. Voir la documentation d’unittest.

Installez-le dans l’environnement virtuel de votre projet avec pip install pytest, puis lancez pytest depuis la racine de votre projet. Sans argument, il découvre et exécute chaque fichier test_*.py qu’il trouve. Options utiles : pytest -q pour une sortie courte, pytest -v pour une ligne par test, pytest test_discounts.py pour exécuter un seul fichier, et pytest test_discounts.py::test_applies_percentage pour exécuter un seul test. Lancez-le après chaque changement — c’est l’habitude qui attrape les régressions tôt.

Une fixture est une configuration réutilisable que pytest fournit à tout test qui la demande. Vous la définissez avec le décorateur @pytest.fixture sur une fonction qui renvoie (ou yield) ce dont les tests ont besoin — des données d’exemple, un fichier temporaire, une connexion de base de données — et tout test qui nomme la fixture comme argument en reçoit une copie fraîche. Les fixtures gardent la configuration à un seul endroit au lieu de la répéter dans chaque test, et pytest en livre des intégrées comme tmp_path (un répertoire temporaire). Voir le guide pratique des fixtures.

Testez vos connaissances

Vous avez cette fonction dans pricing.py :

def with_tax(price, rate):
    if rate < 0:
        raise ValueError("rate must be non-negative")
    return round(price * (1 + rate), 2)

Écrivez un fichier de test, test_pricing.py, qui couvre son comportement.

Tâche 1. Écrivez un test en simple assert vérifiant que with_tax(100, 0.2) renvoie 120.0.

Tâche 2. Utilisez @pytest.mark.parametrize pour vérifier deux cas d’arrondi dans un seul test : with_tax(9.99, 0.1) vaut 10.99, et with_tax(1.005, 0.0) vaut 1.0 (un seul test prenant un price, un rate et un expected).

Tâche 3. Utilisez pytest.raises pour vérifier qu’un taux négatif lève ValueError.

Les imports sont import pytest et from pricing import with_tax. Parametrize avec trois paramètres ressemble à @pytest.mark.parametrize("price,rate,expected", [(9.99, 0.1, 10.99), (1.005, 0.0, 1.0)]), et la signature du test prend (price, rate, expected). Pour le cas d’erreur, enveloppez l’appel dans with pytest.raises(ValueError):.

import pytest
from pricing import with_tax

def test_adds_tax():
    assert with_tax(100, 0.2) == 120.0

@pytest.mark.parametrize("price,rate,expected", [
    (9.99, 0.1, 10.99),
    (1.005, 0.0, 1.0),
])
def test_rounds_to_cents(price, rate, expected):
    assert with_tax(price, rate) == expected

def test_rejects_negative_rate():
    with pytest.raises(ValueError):
        with_tax(100, -0.1)

Lancez pytest -q depuis le dossier qui contient les deux fichiers. Vous devriez voir quatre points (test_adds_tax, les deux cas paramétrés, et test_rejects_negative_rate) et 4 passed.

Une subtilité mérite d’être remarquée : le second cas paramétré attend 1.0, pas 1.01. 1.005 ne peut pas être représenté exactement en virgule flottante binaire, donc round(1.005, 2) voit une valeur un cheveu en dessous de 1.005 et arrondit vers le bas. Ce comportement surprenant-mais-réel est exactement le genre de chose qu’un test verrouille — pour que vous l’appreniez d’une suite verte, pas d’un client déconcerté.

Vérification rapide. Vous enregistrez vos tests dans un fichier appelé checks.py avec des fonctions nommées check_*, vous lancez pytest, et il rapporte « no tests ran ». Qu’est-ce qui ne va pas ?

La découverte des tests. pytest ne collecte que les fichiers nommés test_*.py (ou *_test.py) et les fonctions nommées test_*. Vos noms checks.py / check_* ne correspondent à aucun des deux, donc pytest ne trouve rien à exécuter. Renommez le fichier en test_checks.py et les fonctions en test_*, et elles seront découvertes et exécutées.

Conclusion

Une suite de tests transforme « ça avait l’air correct quand je l’ai exécuté » en « une seule commande me dit ce qui fonctionne encore ». La boucle est petite et toujours la même : écrivez le comportement attendu d’une fonction sous forme de fonction test_* avec un simple assert, lancez pytest, et quand quelque chose échoue, lisez l’introspection des assertions — pytest vous remet la valeur réelle, pas seulement « failed ». À partir de là, @pytest.mark.parametrize couvre de nombreuses entrées dans un seul test, pytest.raises protège le cas d’erreur, et une fixture partage proprement la configuration. Nommez vos fichiers et fonctions test_* pour que la découverte les trouve, lancez la suite après chaque changement, et — surtout quand un assistant a écrit le code — laissez l’exécution qui passe, pas l’extrait à l’air plausible, être votre preuve.

Leçons connexes

Cette page vous a-t-elle été utile ?

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Tester du code Python avec pytest},
  date = {2026-07-18},
  url = {https://www.datanovia.com/learn/programming/python-tools/testing-with-pytest},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Tester du code Python avec pytest.” 2026. July 18. https://www.datanovia.com/learn/programming/python-tools/testing-with-pytest.