pytest — Démarrage rapide
Guide pratique pour installer, écrire et exécuter des tests Python avec pytest.
Ce document couvre les fondamentaux de pytest. Pour la méthodologie générale (pyramide, TDD, mocking), voir Tests — Fondamentaux.
Installation
# Via pip dans un venv
pip install pytest
# Vérifier l'installation
pytest --version# Pour les tests qui utilisent asyncio
pip install pytest-asyncioDécouverte automatique des tests
pytest trouve les tests selon des conventions simples :
| Pattern | Exemple |
|---|---|
| Fichier | test_*.py ou *_test.py |
| Classe | Test* (sans __init__) |
| Fonction | test_* |
| Fixture | fixture_* |
# Lister les tests détectés (sans les exécuter)
pytest --collect-only
# Exécuter tous les tests trouvés
pytestAssertions
pytest surcharge les assertions assert natives de Python. Pas besoin d’importer un framework d’assertion.
# test_calcul.py
def add(a, b):
return a + b
def test_add_positifs():
assert add(2, 3) == 5
def test_add_negatifs():
assert add(-1, -1) == -2
def test_add_melange():
assert add(5, -3) == 2# pytest --tb=short
# PASSED test_calcul.py::test_add_positifs
# PASSED test_calcul.py::test_add_negatifs
# PASSED test_calcul.py::test_add_melangepytest affiche automatiquement la valeur de chaque opérande en cas d’échec, ce qui rend le débogage trivial.
Fixtures
Une fixture est une fonction qui fournit des données ou un contexte réutilisable aux tests.
# test_fixtures.py
import pytest
@pytest.fixture
def sample_list():
return [3, 1, 4, 1, 5, 9]
@pytest.fixture
def sorted_list(sample_list):
return sorted(sample_list)
def test_longueur(sample_list):
assert len(sample_list) == 6
def test_tri(sorted_list):
assert sorted_list == [1, 1, 3, 4, 5, 9]
def test_premier_element(sorted_list):
assert sorted_list[0] == 1Fixtures avec nettoyage
@pytest.fixture
def fichier_temporaire(tmp_path):
"""Crée un fichier temporaire nettoyé après le test."""
f = tmp_path / "test.txt"
f.write_text("données de test")
return f
def test_lecture_fichier(fichier_temporaire):
assert fichier_temporaire.read_text() == "données de test"
# tmp_path est supprimé automatiquement après le testPortée des fixtures
La portée détermine la durée de vie d’une fixture. La valeur par défaut est function : une nouvelle instance est créée à chaque appel de test.
| Portée | Durée de vie | Usage typique |
|---|---|---|
function | Un test | Données de test légères |
class | Toute une classe | État partagé entre tests d’une classe |
module | Un fichier | Configuration de module, fixtures lourdes |
package | Un répertoire | Bases de données partagées entre sous-dossiers |
session | Toute la session | Réseau, services externes, serveur de test |
@pytest.fixture(scope="session")
def base_de_donnees():
"""Lancée une seule fois pour toute la session de tests."""
db = creer_base_test()
yield db
db.fermer()
@pytest.fixture(scope="module")
def client_api(base_de_donnees):
"""Un client par module, partagé entre tous les tests du fichier."""
return creer_client(base_de_donnees)Piège : Une fixture de portée
sessionqui modifie un état global n’est pas réinitialisée entre les modules. Si le test A la modifie, le test B dans un autre module verra cette modification. Utiliserfunctionsi l’état doit être isolé, ou faire un cleanup explicite avecyield.
conftest.py — Fixtures partagées
conftest.py est un fichier spécial qui contient des fixtures accessibles par tous les tests du même dossier (et de ses sous-dossiers). Pas besoin de l’importer explicitement.
projet/
├── conftest.py # Fixtures accessibles dans tout le projet
├── tests/
│ ├── conftest.py # Fixtures accessibles dans tests/
│ ├── test_unitaires/
│ │ ├── conftest.py # Fixtures locales à ce dossier
│ │ └── test_math.py
│ └── test_integration/
│ └── test_api.py# conftest.py (à la racine de tests/)
import pytest
@pytest.fixture
def utilisateur_admin():
"""Utilisateur avec rôle administrateur."""
return {"nom": "Alice", "role": "admin", "actif": True}
@pytest.fixture
def utilisateur_normal():
"""Utilisateur standard."""
return {"nom": "Bob", "role": "user", "actif": True}# tests/test_unitaires/test_auth.py
# Conftest.py à la racine de tests/ est automatiquement détecté.
# Pas besoin de l'importer.
def test_admin_peut_creer(session_utilisateur):
assert session_utilisateur.creer(utilisateur_admin) == "ok"
def test_normal_ne_peut_pas_creer(session_utilisateur):
with pytest.raises(AuthorizationError):
session_utilisateur.creer(utilisateur_normal)Piège :
conftest.pyest chargé une seule fois par pytest, même s’il y en a plusieurs (un par dossier). Les fixtures définies dans unconftest.pyparent sont accessibles depuis les sous-dossiers, mais une fixture de même nom dans un sous-dossier écrase celle du parent (la plus locale a la priorité).
monkeypatch — Variables d’environnement et injection
La fixture monkeypatch permet de modifier temporairement des variables d’environnement, des attributs d’objets ou sys.path le temps d’un test.
# test_config.py
def test_url_api_en_prod(monkeypatch):
monkeypatch.setenv("ENV", "production")
assert config.url_api == "https://api.example.com"
def test_url_api_en_dev(monkeypatch):
monkeypatch.setenv("ENV", "development")
assert config.url_api == "http://localhost:8080"
def test_taux_tva(monkeypatch):
monkeypatch.setattr(config, "tva", 0.20)
assert calculer_prix_ttc(100) == 120.0mocker — Mocks puissants avec pytest-mock
Le plugin pytest-mock expose mocker comme fixture, un wrapper autour de unittest.mock plus intégré à pytest.
pip install pytest-mock# test_service.py
def test_envoi_email(mocker):
mock_envoyer = mocker.patch("app.services.email.envoyer")
mock_envoyer.return_value = True
resultat = Service().envoyer_invitation("alice@example.com")
mock_envoyer.assert_called_once_with("alice@example.com")
assert resultat is True
def test_appel_reseau_lancera_une_exception(mocker):
mocker.patch("app.services.facturation.obtenir_prix",
side_effect=ReseauIndisponible("timeout"))
with pytest.raises(ReseauIndisponible):
Facturation().calculer(1)# Marqueur basique : sauter un test
@pytest.mark.skip(reason="Fonctionnalité pas encore implémentée")
def test_futur():
...
# Paramétrage : exécuter le même test avec plusieurs jeux de données
@pytest.mark.parametrize("entree,attendu", [
("hello", 5),
("pytest", 6),
("", 0),
])
def test_longueur_chaine(entree, attendu):
assert len(entree) == attenduFlags CLI utiles
| Flag | Effet |
|---|---|
-v | Sortie verbeuse (un test par ligne) |
-x | Arrêter au premier échec |
--tb=short | Tracé des erreurs simplifié |
-k "expressions" | Ne lancer que les tests correspondant |
--co | Lister les tests (collect only) |
--last-failed | Relancer les tests ayant échoué précédemment |
-p no:cacheprovider | Désactiver le cache .cache/ |
-s | Afficher le print() (désactiver stdout capture) |
Exemples
# Tests récents qui ont échoué
pytest --last-failed
# Un seul test par nom
pytest -k "test_add_positifs"
# Arrêter au premier échec + verbeux
pytest -x -vExécution dans Docker
# Dockerfile.tests
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Exécuter les tests
CMD ["pytest", "-v", "--tb=short"]# Construire l'image de test
docker build -t mon-projet-tests -f Dockerfile.tests .
# Exécuter les tests
docker run --rm mon-projet-tests
# Montage du code source pour le développement itératif
docker run --rm -v $(pwd):/app mon-projet-testsDocker Compose pour les tests
# docker-compose.test.yml
services:
tests:
build:
context: .
dockerfile: Dockerfile.tests
volumes:
- .:/app
command: ["pytest", "tests/", "-v"]docker compose -f docker-compose.test.yml run --rm testsPièges courants
Piège : Ne pas placer de tests dans un dossier racine arbitraire. Placer les fichiers de test dans un dossier
tests/ou les préfixer partest_pour que pytest les trouve automatiquement.
Piège : Les fixtures sont exécutées à chaque test qui les utilise. Si une fixture modifie un état partagé (fichier global, variable externe), le test devient non indépendant. Créer un état frais dans la fixture et le nettoyer à la fin (utilisez
yieldoutmp_path).
Piège :
assertne fonctionne qu’avec des expressions simples. Pour vérifier un attribut, utiliserassert obj.attr == valeurplutôt queassert obj.is_valide() == True— pytest affiche des messages de comparaison plus lisibles avec les comparaisons directes.
Piège Docker : Les tests qui lancent des services (base de données, serveur) nécessitent un Docker daemon accessible depuis le conteneur. Monter
/var/run/docker.sockdans le conteneur de test (attention aux implications de sécurité en production).