Django REST Framework, généralement abrégé en DRF, est la boîte à outils standard pour la construction d'API au-dessus de Django. Il vous offre des sérialiseurs, des viewsets, des routeurs et une couche d'authentification. Ce qu'il vous offre également, et que de nombreux développeurs sous-utilisent, est une couche de test solide qui s'appuie sur le propre lanceur de tests de Django.
Ce guide montre comment tester une API DRF de deux manières. Premièrement, des tests automatisés écrits en Python utilisant les classes APITestCase et APIClient de DRF, qui s'exécutent sans serveur live et détectent les régressions à chaque commit. Deuxièmement, l'exercice des points de terminaison (endpoints) en fonctionnement avec un client API, ce qui est la manière d'explorer le comportement et de vérifier le service réel. Les deux sont importants et détectent des problèmes différents.
Mettre en place le projet et l'environnement de test
Commencez par un environnement isolé afin que les dépendances de test restent séparées du reste de votre système :
python -m venv venv
source venv/bin/activate
pip install django djangorestframework coverage
Un projet DRF se teste comme n'importe quel projet Django. Par convention, les tests résident dans un fichier tests.py à l'intérieur de chaque application, ou dans un package tests/ si vous en avez plusieurs. Le lanceur de tests de Django découvre toute classe qui hérite d'une variante de TestCase et toute méthode dont le nom commence par test_.
Le choix clé est la classe de base à utiliser. unittest.TestCase n'a pas de base de données. TestCase de Django enveloppe chaque test dans une transaction et la restaure, de sorte que les tests ne se polluent pas mutuellement. APITestCase de DRF étend TestCase de Django et utilise APIClient de DRF, qui comprend l'authentification DRF et les types de contenu. Pour le travail d'API, utilisez APITestCase.
Une autre classe à connaître est TransactionTestCase. Elle n'enveloppe pas les tests dans une transaction, ce qui est nécessaire lorsque le code testé gère lui-même les transactions ou s'appuie sur des fonctionnalités de base de données qu'une transaction enveloppante masquerait. Elle est plus lente car elle tronque les tables entre les tests au lieu de les restaurer, n'y ayez donc recours que lorsque TestCase ne peut réellement pas modéliser le comportement. Pour la grande majorité des tests de points de terminaison, APITestCase est le choix le plus approprié et le plus rapide.
Il est également utile de penser aux données de test dès le début. Utilisez une méthode setUp pour créer les lignes dont chaque test d'une classe a besoin, ou setUpTestData si les données sont en lecture seule et peuvent être partagées entre les classes pour plus de rapidité. Pour les projets plus importants, une bibliothèque de fabriques telle que factory_boy génère des instances de modèle valides sans que vous ayez à spécifier chaque champ, ce qui rend les tests courts et résistants lorsqu'un modèle acquiert un nouveau champ obligatoire.
Tester les sérialiseurs comme des unités
Les sérialiseurs effectuent la validation et la conversion entre les instances de modèle et le JSON. Ils sont petits et purs, ce qui les rend idéaux pour des tests unitaires rapides qui ne touchent pas aux points de terminaison (endpoints).
Supposons que vous ayez un modèle Article et un ArticleSerializer. Un test unitaire vérifie que les données valides passent et que les données invalides échouent :
from django.test import TestCase
from articles.serializers import ArticleSerializer
class ArticleSerializerTests(TestCase):
def test_valid_data_passes(self):
data = {"title": "Caching strategies", "body": "Use ETags."}
serializer = ArticleSerializer(data=data)
self.assertTrue(serializer.is_valid())
def test_missing_title_fails(self):
data = {"body": "No title here."}
serializer = ArticleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn("title", serializer.errors)
Ces tests s'exécutent en millisecondes car il n'y a pas de HTTP ni de vue. Ils vous indiquent que les règles de validation sont correctes avant même d'envisager le point de terminaison. Cette couverture au niveau unitaire est la base de la pyramide de test décrite dans qu'est-ce que le test automatisé.
Tester les points de terminaison avec APITestCase et APIClient
Les tests de points de terminaison vérifient le chemin complet : le routage, la vue, le sérialiseur et la réponse. APIClient de DRF envoie les requêtes en interne (in-process), de sorte qu'aucun serveur ne s'exécute et que les tests restent rapides.
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from articles.models import Article
class ArticleEndpointTests(APITestCase):
def setUp(self):
Article.objects.create(title="First post", body="Hello world")
def test_list_articles_returns_200(self):
url = reverse("article-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_create_article(self):
url = reverse("article-list")
payload = {"title": "Second post", "body": "More content"}
response = self.client.post(url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 2)
Quelques détails méritent d'être notés. self.client est une instance d'APIClient fournie par APITestCase. Utilisez reverse() avec le nom de la route au lieu de coder en dur les URL, afin qu'un changement de route ne casse pas tous les tests. Passez format="json" lors des écritures afin que le client sérialise correctement la charge utile (payload). Affirmez le code de statut avec les constantes nommées de rest_framework.status, car elles sont plus claires que les nombres bruts. Les codes de statut eux-mêmes sont couverts dans le guide sur les codes de statut HTTP que les API REST devraient utiliser.
Tester l'authentification et les permissions
La plupart des API réelles protègent leurs points de terminaison. Vous avez besoin de tests prouvant que les requêtes non authentifiées sont rejetées et que les requêtes authentifiées avec les bonnes permissions réussissent.
APIClient offre deux manières d'authentifier un test. force_authenticate() ignore la vérification des identifiants et attache un utilisateur directement, ce qui est idéal pour tester la logique de la vue elle-même. login() ou credentials() exercent le chemin d'authentification réel. Voici un test de permission utilisant les deux approches :
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse
class ArticlePermissionTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username="editor", password="testpass123"
)
self.url = reverse("article-list")
def test_anonymous_cannot_create(self):
payload = {"title": "Blocked", "body": "Should fail"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_403_FORBIDDEN
)
def test_authenticated_user_can_create(self):
self.client.force_authenticate(user=self.user)
payload = {"title": "Allowed", "body": "Should pass"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_201_CREATED
)
Le premier test confirme que la classe de permission bloque les écritures anonymes. Le second confirme qu'un utilisateur authentifié peut passer. Ensemble, ils définissent la limite de permission, ce qui est exactement le genre de comportement qui se brise discrètement lors des refactorisations. Si vous testez spécifiquement l'authentification par jeton (token), remplacez force_authenticate par self.client.credentials(HTTP_AUTHORIZATION="Token " + token) pour exercer le chemin d'en-tête réel.
Il est important de choisir délibérément laquelle utiliser. force_authenticate est plus rapide et isole la logique de la vue, elle convient donc à la majorité des tests de permission où vous vous souciez uniquement de savoir si un rôle peut ou ne peut pas faire quelque chose. Le chemin d'accès aux identifiants réel est ce que vous voulez pour un ensemble plus restreint de tests qui prouvent spécifiquement que le mécanisme d'authentification fonctionne : qu'un mauvais jeton est rejeté, qu'un jeton expiré échoue, que le point de terminaison de connexion délivre un jeton utilisable. Le mélange des deux vous offre une large couverture à moindre coût et une couverture approfondie là où cela compte.
N'oubliez pas les permissions au niveau des objets. De nombreuses API DRF permettent à un utilisateur de modifier ses propres enregistrements mais pas ceux d'un autre. Testez cela explicitement : créez deux utilisateurs, demandez à l'un de créer un enregistrement, puis affirmez que l'autre utilisateur reçoit un 403 ou 404 lorsqu'il tente de le modifier. Les points de terminaison de liste méritent la même vigilance, car une queryset "fuyante" peut renvoyer des lignes qu'un utilisateur ne devrait jamais voir, même lorsque le point de terminaison de détail est verrouillé.
Exécuter la suite et mesurer la couverture
Exécutez chaque test avec le lanceur de Django :
python manage.py test
Le lanceur découvre vos classes de test, configure une base de données de test jetable, exécute chaque test dans une transaction et annule (rollback) après. Une exécution propre affiche OK. Un échec montre l'assertion qui a échoué et où.
Savoir que la suite passe n'est pas la même chose que savoir qu'elle couvre suffisamment. L'outil coverage mesure quelles lignes ont réellement été exécutées :
coverage run --source='.' manage.py test
coverage report
coverage html
Le rapport liste chaque fichier avec le pourcentage de lignes exercées. La sortie HTML met en évidence les lignes non testées en rouge, ce qui vous indique directement les lacunes. Visez une couverture significative des vues, des sérialiseurs et des permissions plutôt qu'un seul chiffre global. L'article sur comment écrire des scripts de test automatisés couvre ce qui fait qu'un test vaut la peine d'être conservé, car la couverture de tests faibles n'est pas une réelle sécurité.
Tester l'API en direct avec un client
Les tests Python automatisés sont rapides et s'exécutent en CI, mais ils exercent l'application en interne. Ils ne détectent pas les problèmes qui n'apparaissent qu'avec le service en cours d'exécution : un en-tête CORS mal configuré, un proxy inverse qui supprime quelque chose, une base de données lente sous une charge réelle, ou une différence entre votre base de données de test et la production. Pour cela, vous envoyez de vraies requêtes HTTP aux points de terminaison déployés.
Apidog est un excellent choix ici. C'est une plateforme API tout-en-un, vous pouvez donc importer le schéma OpenAPI de votre API DRF, envoyer des requêtes en direct à un serveur en cours d'exécution et construire des assertions visuellement sans écrire plus de Python. DRF peut générer un schéma OpenAPI, et Apidog le consomme directement, ce qui maintient votre client synchronisé avec le contrat réel. Vous pouvez également construire des scénarios de test en plusieurs étapes, par exemple se connecter, créer un article, le récupérer, le supprimer, et les exécuter selon un calendrier ou en CI. Cela complète votre suite APITestCase plutôt que de la remplacer : les tests unitaires et de points de terminaison protègent le code, le client API protège le service déployé. Vous pouvez télécharger Apidog pour importer un schéma DRF et l'essayer. Pour les équipes qui préfèrent rester entièrement en Python, le guide du framework de test automatisé d'API pytest montre comment exécuter des tests de style DRF sous pytest.
Questions fréquemment posées
Quelle est la différence entre TestCase et APITestCase ?
TestCase de Django enveloppe chaque test dans une transaction de base de données et vous fournit un client de test Django standard. APITestCase de DRF en est une sous-classe et remplace le client par APIClient, qui comprend les schémas d'authentification DRF, la négociation de contenu et l'argument format sur les requêtes. Utilisez APITestCase pour tester les points de terminaison DRF.
Quand devrais-je utiliser force_authenticate au lieu de login ?
Utilisez force_authenticate() lorsque vous souhaitez tester la logique de la vue et des permissions sans le coût et la complexité du flux d'authentification réel. Cela attache directement un utilisateur à la requête. Utilisez login() ou credentials() lorsque c'est le mécanisme d'authentification lui-même, tel que l'authentification par session ou par jeton, que vous souhaitez vérifier.
Les tests DRF nécessitent-ils un serveur en cours d'exécution ?
Non. APIClient envoie les requêtes en interne directement à vos vues, de sorte que la suite de tests s'exécute sans démarrer de serveur. C'est ce qui la rend rapide. Pour tester le service réellement déployé, y compris l'infrastructure comme les proxys et CORS, vous envoyez de vraies requêtes HTTP avec un client API tel qu'Apidog.
Comment vérifier la couverture de test pour un projet DRF ?
Installez le package coverage, puis exécutez coverage run --source='.' manage.py test suivi de coverage report pour un résumé ou coverage html pour une vue ligne par ligne. Le rapport HTML met en évidence les lignes non testées afin que vous puissiez voir exactement quelles vues ou quels sérialiseurs manquent de tests.
Devrais-je tester les sérialiseurs et les points de terminaison séparément ?
Oui. Les tests unitaires de sérialiseurs sont rapides et identifient les bogues de validation sans la surcharge HTTP. Les tests de points de terminaison avec APITestCase vérifient le routage, les permissions et le cycle complet de la requête. Conserver les deux vous donne un retour rapide au niveau unitaire et l'assurance que le câblage fonctionne au niveau de l'intégration.
