Skip to Content
Testspytest — Démarrage rapide

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-asyncio

Découverte automatique des tests

pytest trouve les tests selon des conventions simples :

PatternExemple
Fichiertest_*.py ou *_test.py
ClasseTest* (sans __init__)
Fonctiontest_*
Fixturefixture_*
# Lister les tests détectés (sans les exécuter) pytest --collect-only # Exécuter tous les tests trouvés pytest

Assertions

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_melange

pytest 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] == 1

Fixtures 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 test

Porté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éeDurée de vieUsage typique
functionUn testDonnées de test légères
classToute une classeÉtat partagé entre tests d’une classe
moduleUn fichierConfiguration de module, fixtures lourdes
packageUn répertoireBases de données partagées entre sous-dossiers
sessionToute la sessionRé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 session qui 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. Utiliser function si l’état doit être isolé, ou faire un cleanup explicite avec yield.

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.py est chargé une seule fois par pytest, même s’il y en a plusieurs (un par dossier). Les fixtures définies dans un conftest.py parent 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.0

mocker — 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) == attendu

Flags CLI utiles

FlagEffet
-vSortie verbeuse (un test par ligne)
-xArrêter au premier échec
--tb=shortTracé des erreurs simplifié
-k "expressions"Ne lancer que les tests correspondant
--coLister les tests (collect only)
--last-failedRelancer les tests ayant échoué précédemment
-p no:cacheproviderDésactiver le cache .cache/
-sAfficher 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 -v

Exé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-tests

Docker 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 tests

Piè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 par test_ 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 yield ou tmp_path).

Piège : assert ne fonctionne qu’avec des expressions simples. Pour vérifier un attribut, utiliser assert obj.attr == valeur plutôt que assert 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.sock dans le conteneur de test (attention aux implications de sécurité en production).