À quoi ça sert

Tester son code, c'est écrire d'autres morceaux de code qui vérifient que ton code se comporte comme prévu. Pourquoi s'embêter ?

  • Confiance — quand tu modifies une fonction, tu sais immédiatement si tu as cassé autre chose.
  • Documentation vivante — un test montre comment utiliser ta fonction.
  • Détection précoce — un bug attrapé en local coûte 10× moins cher qu'un bug en prod.

Python a un module unittest intégré, mais il est verbeux. pytest propose la même chose en plus simple, avec en bonus des fixtures, du paramétrage et un écosystème énorme de plugins.

En une phrase

pytest = écrire des tests Python en quelques lignes, et savoir en un coup d'œil si ton code marche encore.

Un exemple d'usage

Tu as une fonction qui calcule le prix TTC à partir d'un prix HT. Tu veux t'assurer qu'elle est correcte. Crée un fichier test_prix.py à côté de ton code :

python
from prix import calcul_ttc

def test_ttc_simple():
    assert calcul_ttc(100) == 120.0

def test_ttc_zero():
    assert calcul_ttc(0) == 0

def test_ttc_refuse_negatif():
    import pytest
    with pytest.raises(ValueError):
        calcul_ttc(-10)

Tu lances pytest dans le terminal, et tu obtiens :

sortie console
collected 3 items

test_prix.py ...                                          [100%]

============== 3 passed in 0.02s ===============

Trois tests passent. Si tu casses la fonction, pytest pointe la ligne assert qui échoue, avec la valeur attendue et la valeur obtenue.

How-to : installer et utiliser pytest

  1. Installer pytest comme dépendance de dev

    Avec UV (voir la fiche UV) :

    bash
    uv add --dev pytest

    Ou avec pip :

    bash
    pip install pytest
    Astuce

    Le flag --dev indique que pytest n'est pas une dépendance de production : on n'en a besoin que pour développer et tester.

  2. Structurer ton projet

    pytest suit une convention simple :

    arbo
    mon-projet/
    ├── src/
    │   └── calculs.py
    ├── tests/
    │   └── test_calculs.py
    └── pyproject.toml

    pytest découvre automatiquement les fichiers test_*.py ou *_test.py, et dans chacun, les fonctions commençant par test_.

  3. Écrire un premier test

    Fichier src/calculs.py :

    python
    def addition(a, b):
        return a + b
    
    def division(a, b):
        if b == 0:
            raise ValueError("division par zéro")
        return a / b

    Fichier tests/test_calculs.py :

    python
    import pytest
    from calculs import addition, division
    
    def test_addition():
        assert addition(2, 3) == 5
    
    def test_division_normale():
        assert division(10, 2) == 5
    
    def test_division_par_zero():
        with pytest.raises(ValueError, match="division par zéro"):
            division(10, 0)
  4. Lancer les tests

    bash
    # Tous les tests
    uv run pytest
    
    # Mode verbeux (un nom de test par ligne)
    uv run pytest -v
    
    # Un seul fichier
    uv run pytest tests/test_calculs.py
    
    # Un seul test
    uv run pytest tests/test_calculs.py::test_addition
    
    # Filtre par nom (mots-clés)
    uv run pytest -k "division and not zero"
    
    # S'arrêter au premier échec
    uv run pytest -x
  5. Paramétrer un test (data-driven)

    Plutôt que d'écrire 5 fois le même test avec des valeurs différentes, utilise parametrize :

    python
    import pytest
    from calculs import addition
    
    @pytest.mark.parametrize("a, b, attendu", [
        (2, 3, 5),
        (0, 0, 0),
        (-1, 1, 0),
        (1.5, 2.5, 4.0),
    ])
    def test_addition(a, b, attendu):
        assert addition(a, b) == attendu

    pytest crée automatiquement 4 tests à partir de cette définition.

  6. Utiliser des fixtures (données partagées)

    Une fixture prépare un objet réutilisable par plusieurs tests (un fichier temporaire, une connexion DB, un client HTTP…).

    python
    import pytest
    
    @pytest.fixture
    def utilisateur():
        return {"nom": "Alice", "role": "admin"}
    
    def test_nom(utilisateur):
        assert utilisateur["nom"] == "Alice"
    
    def test_role(utilisateur):
        assert utilisateur["role"] == "admin"

    pytest voit le paramètre utilisateur dans la signature et injecte automatiquement la valeur retournée par la fixture du même nom.

    Fixtures partagées

    Place tes fixtures communes dans tests/conftest.py : pytest les rendra disponibles dans tous les fichiers de test sans import.

  7. Mesurer la couverture (coverage)

    Pour savoir quelle proportion de ton code est testée, ajoute le plugin pytest-cov :

    bash
    uv add --dev pytest-cov
    uv run pytest --cov=src --cov-report=term-missing

    Tu vois ligne par ligne ce qui est couvert et ce qui ne l'est pas. --cov-report=html génère un rapport HTML navigable.

  8. Configurer VS Code

    1. Installe l'extension Python de Microsoft.
    2. Ouvre la palette (Ctrl+Shift+P) → Python: Configure Tests.
    3. Choisis pytest puis le dossier tests.
    4. Une icône « bécher » apparaît dans la barre latérale : tu y vois tous tes tests, tu peux les lancer ou les déboguer en un clic.

    Pour figer la config dans le projet, ajoute à pyproject.toml :

    toml
    [tool.pytest.ini_options]
    testpaths = ["tests"]
    pythonpath = ["src"]
    addopts = "-v --tb=short"

    pythonpath évite les imports cassés quand tes tests sont dans un dossier séparé du code.

Aide-mémoire

bash
pytest                       # tous les tests
pytest -v                    # détaillé
pytest -x                    # stop au 1er échec
pytest -k "motif"            # filtre par nom
pytest --lf                  # rejouer uniquement les derniers échoués
pytest -s                    # affiche les print()
pytest --pdb                 # debugger sur échec
pytest --cov=src             # couverture (avec pytest-cov)

Marqueurs utiles

python
@pytest.mark.skip(reason="non implémenté")
@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
@pytest.mark.xfail                   # attendu en échec
@pytest.mark.parametrize(...)        # multi-valeurs

Pour aller plus loin