Les développeurs Python se tournent vers pytest car il se fait oublier. Un test est simplement une fonction dont le nom commence par test_, une assertion est une simple instruction assert, et le runner fait le reste. Associez-le à la bibliothèque requests et vous obtenez un framework complet, axé sur le code, pour automatiser les tests d'API, sans cérémonial lourd.
Ce tutoriel montre comment construire une véritable suite de tests d'API pytest. Vous mettrez en place le projet, écrirez votre premier test de requête, partagerez la logique de configuration avec des fixtures, exécuterez le même test sur de nombreuses entrées avec parametrize, et ferez des assertions sur le statut de la réponse, le corps et le schéma JSON. Chaque exemple utilise une API de style public réaliste afin que vous puissiez adapter le code directement.
Configuration du projet
Installez les deux bibliothèques dont vous avez besoin dans un environnement virtuel :
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Une structure claire permet de maintenir la suite à mesure qu'elle grandit :
api-tests/
conftest.py # fixtures partagées
test_users.py # tests pour les points d'accès utilisateurs
test_orders.py # tests pour les points d'accès commandes
pytest.ini # configuration
Pytest découvre les tests automatiquement. Les fichiers doivent commencer par test_ ou se terminer par _test.py, les fonctions doivent commencer par test_, et les classes de test doivent commencer par Test et ne pas avoir de méthode __init__. Suivez ces règles et vous n'aurez jamais à configurer la découverte manuellement. Si les tests automatisés en tant que discipline sont nouveaux pour vous, notre introduction sur ce qu'est le test automatisé vous mettra en contexte.
Pourquoi pytest pour les tests d'API spécifiquement ? La bibliothèque requests gère le HTTP, et pytest gère tout le reste : la découverte, les assertions avec une sortie d'échec lisible, la configuration et le nettoyage via les fixtures, les exécutions pilotées par les données via parametrize et le reporting. Vous assemblez un framework complet d'automatisation d'API à partir de deux petites bibliothèques bien documentées, dans une langue que votre équipe utilise probablement déjà pour l'application elle-même. Cette proximité compte. Les tests vivant dans le même dépôt que le code restent pertinents, car un changement cassant et son test échouant apparaissent dans la même pull request.
Écrire votre premier test d'API
Un test d'API pytest envoie une requête et fait des assertions sur la réponse. Voici un test pour un point d'accès utilisateurs :
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
Exécutez la suite avec pytest -v. Chaque assert qui échoue produit un rapport détaillé montrant la valeur réelle, ce qui est l'une des meilleures fonctionnalités de pytest. Vous n'avez pas besoin de méthodes d'assertion spéciales ; le framework réécrit les simples instructions assert pour donner une sortie riche. Pour l'ensemble plus large des vérifications à effectuer sur une réponse, consultez notre guide sur les assertions d'API.
Partager la configuration avec les fixtures
Répéter l'URL de base et une session HTTP dans chaque test est un gaspillage. Les fixtures résolvent ce problème. Une fixture est une fonction décorée avec @pytest.fixture qui produit une valeur que les tests peuvent demander en la nommant comme paramètre.
Placez les fixtures partagées dans conftest.py afin que chaque fichier de test puisse les utiliser sans importation :
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
L'argument scope="session" signifie que la session est créée une seule fois pour l'exécution entière au lieu d'une fois par test. Le mot-clé yield sépare la configuration du nettoyage : le code avant yield s'exécute en premier, le code après s'exécute lorsque la fixture sort de sa portée. Un test demande simplement ce dont il a besoin :
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
Les fixtures sont le remplacement moderne de pytest pour le style plus ancien setup_function et teardown_function. Elles se composent proprement, supportent les scopes et rendent les dépendances explicites, c'est pourquoi la documentation officielle des fixtures pytest les recommande comme approche par défaut.
Exécuter un test sur de nombreuses entrées
Les points d'accès API doivent généralement être vérifiés avec de nombreuses entrées : des valeurs valides, des valeurs invalides et des cas limites. Écrire une fonction séparée pour chacun est fastidieux. Le décorateur @pytest.mark.parametrize exécute un corps de test sur une liste d'entrées :
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
Ceci produit quatre cas de test distincts à partir d'une seule fonction. Chacun s'exécute et rapporte indépendamment, de sorte qu'une seule mauvaise entrée n'en cache pas les autres. Parametrize est la réponse intégrée de pytest aux tests pilotés par les données. Lorsque l'ensemble d'entrées devient important, chargez-le plutôt à partir d'un fichier ; notre guide sur les tests d'API pilotés par les données avec CSV et JSON couvre ce modèle. Si vous ne savez pas quel code de statut chaque entrée doit renvoyer, la référence sur les codes de statut HTTP que les API REST devraient utiliser est un compagnon utile.
Assertions sur le corps de la réponse et le schéma
Les codes de statut sont nécessaires mais pas suffisants. Une réponse 200 avec un corps mal formé est toujours un bug. Faites une assertion directement sur le JSON analysé :
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
Pour des garanties plus solides, validez le corps par rapport à un schéma JSON. Cela détecte les dérives structurelles, telles qu'un champ renommé ou manquant, que les vérifications manuelles manquent :
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
La validation de schéma s'adapte mieux que les assertions champ par champ car un seul schéma couvre toute la forme de la réponse. La bibliothèque jsonschema est le choix standard, et sa documentation de validation explique les mots-clés pris en charge.
Exécuter la suite en CI
Une suite pytest prend tout son sens lorsqu'elle s'exécute automatiquement. Pytest renvoie un code de sortie non nul en cas d'échec, ce qui est exactement ce dont un serveur CI a besoin pour faire échouer une construction. Émettez un rapport JUnit pour un affichage en ligne :
pytest -v --junitxml=results.xml
Connectez cette commande à une étape GitHub Actions ou à tout autre pipeline et vos tests d'API protègent chaque commit. Notre présentation des tests d'API dans les pipelines CI/CD montre la configuration complète, y compris l'injection de secrets pour les jetons et la sélection d'environnement.
Deux habitudes CI maintiennent une suite pytest fiable. Premièrement, ne jamais coder en dur les secrets ou les URL d'environnement dans les fichiers de test. Lisez-les à partir de variables d'environnement afin que la même suite s'exécute en staging en CI et localement sans modifications :
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Deuxièmement, exécutez les tests indépendants en parallèle pour que les retours soient rapides. Le plugin pytest-xdist répartit les tests sur les cœurs du CPU avec pytest -n auto. Les exécutions parallèles ne fonctionnent que si vos tests ne partagent pas d'état, ce qui est une raison de plus pour laquelle la couche de données de test est importante. Une suite qui dépend de l'ordre d'exécution échouera de manière imprévisible au moment où elle s'exécutera en parallèle.
Maintenir une suite pytest
Une suite de cinquante tests est facile. Une suite de cinq cents demande de la discipline. Trois pratiques maintiennent un framework API pytest sain à mesure qu'il grandit.
Regroupez les tests liés en modules et utilisez les classes uniquement lorsqu'elles partagent la configuration, pas pour la décoration. Un fichier test_orders.py avec un ensemble clair de fonctions est plus lisible qu'un fichier géant. Utilisez des marques, enregistrées dans pytest.ini, pour étiqueter les tests afin de pouvoir exécuter des sous-ensembles : @pytest.mark.smoke pour un contrôle rapide, @pytest.mark.slow pour l'ensemble complet. Exécutez l'ensemble smoke à chaque commit et l'ensemble complet chaque nuit.
Centralisez la configuration. Les URL de base, les schémas et les fixtures partagées appartiennent à conftest.py ou à un petit module de configuration, jamais copiés-collés entre les fichiers. Lorsque l'URL de staging change, vous ne devriez avoir à modifier qu'une seule ligne. La même discipline modulaire qui s'applique à tout framework, couverte dans notre guide sur comment écrire des scripts de test automatisés, s'applique ici : extrayez tout ce que vous écrivez deux fois dans une fixture ou une fonction d'aide.
Quand se tourner plutôt vers une plateforme
Un framework pytest est excellent lorsque votre équipe écrit en Python et souhaite que les tests résident à côté du code de l'application. Il est moins pratique lorsque le personnel QA ou produit doit contribuer, ou lorsque vous souhaitez la conception de tests, le mocking et l'exécution en un seul endroit sans maintenir de code "glue".
Apidog comble ce vide. Il offre une construction visuelle de tests, une validation de schéma par rapport à votre spécification OpenAPI, des exécutions pilotées par les données à partir de CSV et JSON, et un runner CLI pour la CI, le tout sans écrire de code de fixture et d'assertion à la main. De nombreuses équipes utilisent les deux : pytest pour les scénarios à forte logique et Apidog pour une couverture large et pour concevoir et simuler les API que la suite pytest teste. Vous pouvez télécharger Apidog et comparer les deux approches sur un point d'accès réel en un après-midi.
Questions fréquemment posées
Pourquoi utiliser pytest plutôt que le module unittest intégré de Python pour les tests d'API ?
Pytest nécessite moins de boilerplate. Les tests sont de simples fonctions, les assertions sont de simples instructions assert avec une sortie d'échec riche, et les fixtures gèrent la configuration plus souplement que les méthodes basées sur les classes de unittest. Pytest dispose également d'un vaste écosystème de plugins et d'un parametrize intégré pour les tests pilotés par les données. Il peut toujours exécuter des tests de style unittest existants, donc la migration présente peu de risques.
Quelle est la différence entre une fixture et parametrize ?
Une fixture fournit une ressource réutilisable, telle qu'une session HTTP ou un jeton d'authentification, à tout test qui la demande. Parametrize exécute le même corps de test plusieurs fois avec différentes valeurs d'entrée. Les fixtures partagent la configuration ; parametrize multiplie les cas. Ils se combinent bien : un test paramétré peut toujours dépendre de fixtures.
Devrais-je faire des assertions sur le temps de réponse dans les tests d'API pytest ?
Vous pouvez, en utilisant response.elapsed.total_seconds(), et une limite supérieure lâche permet de détecter les régressions importantes. Mais pytest est un outil de test fonctionnel, pas un testeur de charge. Pour un véritable travail de performance, utilisez un outil dédié. Gardez les assertions de temps généreuses afin que la variance normale du réseau ne provoque pas d'échecs instables.
Comment garder les tests d'API indépendants dans pytest ?
Donnez à chaque test ses propres données via des fixtures qui créent et nettoient les ressources, et évitez de dépendre de l'ordre d'exécution des tests. Pytest exécute les tests dans l'ordre des fichiers par défaut, mais une suite bien conçue n'en dépend pas. Les tests indépendants peuvent s'exécuter en parallèle et échouer de manière isolée, ce qui facilite grandement le débogage.
Pytest peut-il valider les réponses par rapport à une spécification OpenAPI ?
Pytest lui-même ne le fait pas, mais vous pouvez valider par rapport à un schéma JSON avec la bibliothèque jsonschema, et il existe des plugins qui vérifient les réponses par rapport à un document OpenAPI. Si la validation de schéma est centrale à votre flux de travail, une plateforme comme Apidog qui valide automatiquement par rapport à votre spécification OpenAPI peut vous épargner la configuration des plugins.
