16. Tests et Packaging

unittest, pytest, logging, pyproject.toml, uv, publication.


16.1 unittest

Module de test intégré :

# test_maths.py
import unittest
from mon_module import addition, division
 
class TestMaths(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(addition(2, 3), 5)
        self.assertEqual(addition(-1, 1), 0)
 
    def test_division(self):
        self.assertEqual(division(10, 2), 5)
        with self.assertRaises(ValueError):
            division(10, 0)
 
if __name__ == "__main__":
    unittest.main()
python3 -m unittest test_maths.py
python3 -m unittest discover  # découverte automatique

16.2 pytest

Plus concis et puissant :

# test_maths.py
from mon_module import addition, division
 
def test_addition():
    assert addition(2, 3) == 5
    assert addition(-1, 1) == 0
 
def test_division():
    assert division(10, 2) == 5
 
def test_division_par_zero():
    with pytest.raises(ValueError):
        division(10, 0)
pip install pytest
pytest                              # découverte automatique
pytest test_maths.py -v             # verbeux
pytest -k "addition"                # filtre par nom
pytest --cov=mon_module tests/      # couverture (pytest-cov)

Fixtures

import pytest
 
@pytest.fixture
def données():
    return {"nom": "Alice", "âge": 30}
 
def test_traitement(données):
    resultat = traiter(données)
    assert resultat["nom"] == "ALICE"

Paramétrisation

@pytest.mark.parametrize("a,b,attendu", [
    (1, 2, 3),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_addition(a, b, attendu):
    assert addition(a, b) == attendu

Mocking

from unittest.mock import Mock, patch
 
def test_api():
    with patch("mon_module.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"id": 1}
        resultat = mon_appel_api()
        assert resultat["id"] == 1

16.3 logging

import logging
 
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
 
logger = logging.getLogger(__name__)
 
logger.debug("Détail de débogage")
logger.info("Opération réussie")
logger.warning("Attention")
logger.error("Erreur critique")
logger.critical("Fatal")

Bonnes pratiques :

  • Utiliser logging pas print pour les messages du programme.
  • Configurer le niveau par environnement (DEBUG, INFO, WARNING).
  • Utiliser getLogger(__name__) par module.

Configuration fichier

logging.config.dictConfig({
    "version": 1,
    "handlers": {
        "fichier": {
            "class": "logging.FileHandler",
            "filename": "app.log",
        },
    },
    "root": {
        "handlers": ["fichier"],
        "level": "INFO",
    },
})

16.4 Packaging avec pyproject.toml

Structure minimale :

mon_projet/
├── pyproject.toml
├── src/
│   └── mon_paquet/
│       ├── __init__.py
│       └── module.py
├── tests/
│   └── test_module.py
└── README.md

pyproject.toml :

[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.backends._legacy:_Backend"
 
[project]
name = "mon-paquet"
version = "0.1.0"
description = "Description courte"
requires-python = ">=3.10"
dependencies = [
    "numpy>=1.26",
    "requests>=2.32",
]
 
[project.optional-dependencies]
dev = [
    "pytest>=8",
    "pytest-cov>=5",
    "mypy>=1.10",
]
 
[tool.pytest.ini_options]
testpaths = ["tests"]

16.5 uv — gestionnaire moderne

uv init mon_projet         # créer un projet
uv add numpy               # ajouter dépendance
uv add --dev pytest        # dépendance de développement
uv run python script.py    # exécuter dans l'environnement
uv build                   # construire le paquet
uv publish                 # publier sur PyPI

16.6 Tests de performance

import timeit
 
# Temps d'exécution
timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
 
# Depuis pytest
def test_perf():
    début = time.perf_counter()
    fonction_lourde()
    durée = time.perf_counter() - début
    assert durée < 1.0  # moins d'1 seconde

🔗 ← Retour au cours · ← précédent · suivant →