Pourquoi Pytest ?
Python propose plusieurs frameworks de tests (unittest, nose, doctest), mais Pytest s’est imposĂ© comme la rĂ©fĂ©rence pour sa simplicitĂ©, sa puissance et son Ă©cosystĂšme riche. Contrairement Ă unittest, Pytest vous permet d’Ă©crire des tests sans boilerplate â pas de classes obligatoires, pas de mĂ©thodes spĂ©ciales, juste des fonctions assert natives.
Dans ce tutoriel, vous apprendrez Ă :
- Installer et configurer Pytest
- Ăcrire vos premiers tests
- Maßtriser les fixtures et la paramétrisation
- Utiliser le mocking avec monkeypatch et pytest-mock
- Mesurer la couverture de code
- Intégrer Pytest dans une CI/CD
Installation et premiers pas
Installez Pytest et ses extensions courantes :
pip install pytest pytest-cov pytest-mock
VĂ©rifiez l’installation :
pytest --version
Créez un fichier test_demo.py :
def test_addition():
assert 1 + 1 == 2
def test_string():
assert "hello".upper() == "HELLO"
Lancez les tests :
pytest
Pytest découvre automatiquement les fichiers nommés test_*.py ou *_test.py et exécute les fonctions préfixées par test_. En mode verbeux :
pytest -v
Assertions avancées
Pytest fonctionne avec l’assert natif de Python, mais enrichit les messages d’erreur grĂące Ă l’introspection :
def test_liste():
resultat = [1, 2, 3]
attendu = [1, 2, 4]
assert resultat == attendu
ExĂ©cutez ce test : Pytest vous montrera exactement quels Ă©lĂ©ments diffĂšrent, contrairement Ă unittest qui se contente d’un boolĂ©en.
Pour tester les exceptions :
import pytest
def test_division_par_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_erreur_avec_message():
with pytest.raises(ValueError, match="doit ĂȘtre positif"):
raise ValueError("le nombre doit ĂȘtre positif")
Les fixtures : le cĆur de Pytest
Les fixtures permettent de préparer un contexte (base de données, fichiers, API) réutilisable entre les tests :
import pytest
@pytest.fixture
def utilisateur_test():
"""Crée un dictionnaire utilisateur factice."""
return {"nom": "Alice", "age": 30, "actif": True}
def test_utilisateur_actif(utilisateur_test):
assert utilisateur_test["actif"] is True
def test_age_utilisateur(utilisateur_test):
assert utilisateur_test["age"] >= 18
Portées (scope) des fixtures
Les fixtures peuvent ĂȘtre partagĂ©es Ă diffĂ©rentes Ă©chelles :
function(dĂ©faut) â recréée Ă chaque testclassâ une instance par classe de testmoduleâ une fois par modulesessionâ une fois pour toute la session de test
@pytest.fixture(scope="session")
def connexion_bdd():
"""Connexion partagée entre tous les tests de la session."""
conn = creer_connexion()
yield conn
conn.fermer()
Notez l’utilisation de yield pour le nettoyage : le code aprĂšs yield est exĂ©cutĂ© Ă la fin du test, quel que soit le rĂ©sultat.
Paramétrisation
Ăvitez la duplication de tests avec @pytest.mark.parametrize :
import pytest
def multiplier(a, b):
return a * b
@pytest.mark.parametrize("a,b,attendu", [
(2, 3, 6),
(0, 5, 0),
(-1, 10, -10),
(100, 0, 0),
])
def test_multiplier(a, b, attendu):
assert multiplier(a, b) == attendu
Vous pouvez aussi combiner plusieurs niveaux de paramétrisation :
@pytest.mark.parametrize("a", [1, 2, 3])
@pytest.mark.parametrize("b", [10, 20])
def test_produit_cartesien(a, b):
assert isinstance(a * b, int)
Mocking avec monkeypatch
monkeypatch est une fixture intĂ©grĂ©e Ă Pytest qui permet de modifier temporairement des objets, variables d’environnement ou comportements :
import os
import pytest
def get_api_key():
return os.environ.get("API_KEY", "default")
def test_api_key_personnalisee(monkeypatch):
monkeypatch.setenv("API_KEY", "secret-123")
assert get_api_key() == "secret-123"
def test_api_key_defaut(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
assert get_api_key() == "default"
Pour mocker des fonctions ou classes :
import requests
def obtenir_utilisateur(url, user_id):
reponse = requests.get(f"{url}/users/{user_id}")
return reponse.json()
def test_obtenir_utilisateur(monkeypatch):
class FakeReponse:
@staticmethod
def json():
return {"id": 1, "nom": "Alice"}
def fake_get(url):
return FakeReponse()
monkeypatch.setattr(requests, "get", fake_get)
resultat = obtenir_utilisateur("https://api.exemple.com", 1)
assert resultat["nom"] == "Alice"
Mocking avancé avec pytest-mock
L’extension pytest-mock apporte une API plus expressive basĂ©e sur unittest.mock :
def test_mock_requests(mocker):
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"id": 1, "nom": "Bob"}
resultat = obtenir_utilisateur("https://api.exemple.com", 1)
assert resultat["nom"] == "Bob"
mock_get.assert_called_once_with("https://api.exemple.com/users/1")
Vérifications avancées
def test_spy_sur_methode(mocker):
from datetime import datetime
spy = mocker.spy(datetime, "now")
maintenant = datetime.now()
spy.assert_called_once()
assert isinstance(maintenant, datetime)
Couverture de code avec pytest-cov
Mesurez quel pourcentage de votre code est couvert par les tests :
pytest --cov=mon_package --cov-report=html --cov-report=term
Options utiles :
--cov=mon_packageâ mesure la couverture du package cible--cov-report=termâ affiche le rĂ©sumĂ© dans le terminal--cov-report=htmlâ gĂ©nĂšre un rapport HTML danshtmlcov/--cov-fail-under=80â Ă©choue si la couverture est infĂ©rieure Ă 80 %
Organisation des tests
Structure recommandée pour un projet Python :
mon_projet/
âââ src/
â âââ mon_package/
â âââ __init__.py
â âââ calculs.py
â âââ services.py
âââ tests/
â âââ __init__.py
â âââ conftest.py # fixtures partagĂ©es
â âââ test_calculs.py
â âââ test_services.py
âââ pytest.ini
âââ pyproject.toml
Fichier pytest.ini minimal :
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
markers =
slow: tests lents (exécution optionnelle)
integration: tests d'intégration
Exécution sélective :
pytest -m "not slow" # ignore les tests lents
pytest -m "integration" # seulement les tests d'intégration
pytest tests/test_calculs.py::test_multiplier # un test spécifique
pytest -k "multiplier" # filtre par expression
Intégration CI/CD (GitHub Actions)
Exemple de workflow GitHub Actions :
.github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pip install pytest pytest-cov
- run: pytest --cov=mon_package --cov-fail-under=80
Bonnes pratiques
- Un test = une assertion logique : testez un comportement Ă la fois
- Nommage explicite :
test_xx_quand_y_alors_zplutÎt quetest_01 - Fixtures scope session : pour les ressources coûteuses (connexions BDD, clients API)
- conftest.py : regroupez les fixtures partagées entre plusieurs fichiers de test
- –strict-markers : activez cette option pour Ă©viter les typos dans les marqueurs
- TDD (Test-Driven Development) : Ă©crivez le test avant l’implĂ©mentation
Aller plus loin
Pytest dispose d’un Ă©cosystĂšme riche d’extensions :
pytest-xdistâ exĂ©cution parallĂšle des testspytest-asyncioâ tests pour code asynchronepytest-django/pytest-flaskâ tests pour frameworks webpytest-benchmarkâ benchmarks de performancepytest-timeoutâ limite de temps par testpytest-sugarâ affichage amĂ©liorĂ© dans le terminal
Documentation officielle : https://docs.pytest.org/