Tests automatisés d'API avec PHPUnit et Laravel HTTP Tests
Introduction
Dans le monde du développement backend, les API (Interfaces de Programmation Applicative) sont les piliers sur lesquels reposent nos applications modernes, qu'elles soient mobiles, web (SPA) ou même des services tiers. Assurer leur fiabilité, leur robustesse et leur bon fonctionnement est donc crucial. C'est ici qu'interviennent les tests automatisés.
Les tests automatisés ne sont pas un luxe, mais une nécessité. Ils permettent de :
- Valider la logique métier : S'assurer que votre API se comporte comme attendu face à différentes entrées.
- Détecter les régressions : Capturer rapidement les bugs introduits par de nouvelles fonctionnalités ou des refactorings.
- Améliorer la maintenabilité : Des tests bien écrits servent de documentation implicite et facilitent la compréhension du code.
- Accélérer le développement : Moins de tests manuels, plus de confiance lors des déploiements.
Laravel, avec son écosystème riche, offre des outils puissants pour les tests, notamment PHPUnit pour le framework de test et les Laravel HTTP Tests (parfois appelés "Feature Tests") qui simplifient la simulation de requêtes HTTP. Ce cours vous guidera à travers l'art de tester vos API Laravel, en exploitant pleinement ces outils.
Rappel rapide sur PHPUnit
PHPUnit est le framework de test unitaire de facto pour PHP. Il fournit une structure pour écrire des tests, des assertions pour vérifier les résultats, et un runner pour exécuter ces tests. Laravel intègre PHPUnit par défaut, ce qui le rend immédiatement utilisable dès l'installation.
Prérequis et Setup
Laravel pré-configure PHPUnit pour vous. Lorsque vous installez une nouvelle application Laravel, vous trouverez les fichiers nécessaires dans le dossier tests/.
Structure des tests Laravel
tests/Unit: Contient les tests unitaires purs, qui testent des classes ou des méthodes isolées, sans dépendances externes (base de données, HTTP, etc.).tests/Feature: Contient les tests fonctionnels ou d'intégration. C'est ici que nous écrirons nos tests d'API. Ils testent la collaboration entre plusieurs composants, simulent des requêtes HTTP et interagissent souvent avec la base de données.
Configuration du fichier .env.testing
Pour garantir l'isolation de vos tests et éviter d'impacter votre base de données de développement ou de production, Laravel utilise un fichier d'environnement spécifique pour les tests : .env.testing.
Par défaut, Laravel est configuré pour utiliser une base de données SQLite en mémoire pour les tests, ce qui est idéal pour la vitesse et l'isolation.
# .env.testing
APP_ENV=testing
APP_DEBUG=true
LOG_CHANNEL=stack
DB_CONNECTION=sqlite
DB_DATABASE=:memory: # Utilise une base de données SQLite en mémoire
Si ce fichier n'existe pas, vous pouvez le créer en copiant votre .env et en ajustant les variables pour les tests.
Comprendre les Tests d'API (Feature Tests)
Les tests d'API sont un type de Feature Tests. Leur objectif est de vérifier que votre API répond correctement aux requêtes HTTP, en termes de :
- Statut HTTP (200 OK, 201 Created, 404 Not Found, 422 Unprocessable Entity, etc.).
- Contenu de la réponse (format JSON, données spécifiques, structure).
- Interactions avec la base de données (création, mise à jour, suppression de données).
- Comportement en cas d'erreur (validation des entrées, erreurs d'authentification/autorisation).
La classe Illuminate\Foundation\Testing\TestCase est la base de tous vos tests de fonctionnalité Laravel. Elle fournit des méthodes utilitaires pour simuler des requêtes HTTP, gérer les sessions, les utilisateurs authentifiés, et bien plus encore.
Écrire votre Premier Test d'API
Commençons par un exemple concret : tester un endpoint simple qui retourne une liste d'utilisateurs.
Génération d'un test
Laravel propose une commande Artisan pour générer rapidement une nouvelle classe de test.
php artisan make:test UserApiTest --feature
Cette commande créera un fichier tests/Feature/UserApiTest.php avec la structure de base. L'option --feature place le test dans le bon dossier et hérite de la classe TestCase.
Un test GET simple : Lister les utilisateurs
Supposons que vous ayez un endpoint GET /api/users qui renvoie une liste d'utilisateurs au format JSON.
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User; // Assurez-vous d'importer votre modèle User
class UserApiTest extends TestCase
{
use RefreshDatabase; // Réinitialise la base de données avant chaque test
/**
* Teste la récupération de la liste des utilisateurs.
*
* @return void
*/
public function test_it_can_list_users(): void
{
// Créer quelques utilisateurs pour les besoins du test
User::factory()->count(3)->create();
// Envoyer une requête GET à l'API
$response = $this->getJson('/api/users');
// Assurer que le statut HTTP est 200 (OK)
$response->assertStatus(200);
// Assurer que la réponse JSON contient 3 éléments au niveau racine
$response->assertJsonCount(3);
// Assurer que la structure JSON est correcte
$response->assertJsonStructure([
'*' => [ // Pour chaque élément du tableau
'id',
'name',
'email',
// 'email_verified_at', // peut être nullable ou non retourné
'created_at',
'updated_at',
]
]);
// Assurer qu'au moins un fragment de données spécifiques est présent
$response->assertJsonFragment([
'email' => User::first()->email,
]);
}
}
Explication du code
use RefreshDatabase;: Ce trait est essentiel pour les tests d'intégration. Il s'assure que la base de données est migrée et réinitialisée (vidée) avant l'exécution de chaque méthode de test. Cela garantit que chaque test s'exécute dans un environnement propre et indépendant.User::factory()->count(3)->create();: Laravel Model Factories sont utilisées pour créer facilement des instances de modèles et les persister en base de données avec des données factices. Ici, nous créons 3 utilisateurs.$this->getJson('/api/users');: C'est la méthode clé pour simuler une requête HTTP GET. La variantegetJson()est spécifiquement conçue pour les API qui attendent et renvoient du JSON. Elle ajoute automatiquement l'en-têteAccept: application/json.- D'autres méthodes existent pour différents verbes HTTP :
$this->postJson(),$this->putJson(),$this->deleteJson(), etc.
- D'autres méthodes existent pour différents verbes HTTP :
$response->assertStatus(200);: Vérifie que le code de statut HTTP de la réponse est 200 (OK).$response->assertJsonCount(3);: Vérifie que le tableau JSON retourné contient exactement 3 éléments à son niveau racine.$response->assertJsonStructure([...]);: Vérifie que la structure de la réponse JSON est conforme à celle attendue. Le*indique que cette structure s'applique à chaque élément d'un tableau.$response->assertJsonFragment([...]);: Vérifie qu'au moins un objet de la réponse JSON contient les paires clé-valeur spécifiées. Utile pour s'assurer que des données spécifiques sont bien présentes.
Tests d'API plus Avancés
Au-delà de la simple récupération, les API impliquent souvent la création, la mise à jour et la suppression de ressources, ainsi que la gestion de l'authentification.
Tests POST : Création de ressources
Pour tester la création d'une ressource (ex: un nouvel utilisateur), nous devons simuler une requête POST avec des données.
<?php
// ... (en haut du fichier UserApiTest.php)
class UserApiTest extends TestCase
{
use RefreshDatabase;
/**
* Teste la création d'un nouvel utilisateur.
*
* @return void
*/
public function test_it_can_create_a_user(): void
{
$userData = [
'name' => 'John Doe',
'email' => 'john.doe@example.com',
'password' => 'password',
'password_confirmation' => 'password',
];
$response = $this->postJson('/api/users', $userData);
$response->assertStatus(201); // 201 Created
$response->assertJsonFragment([
'name' => 'John Doe',
'email' => 'john.doe@example.com',
]);
// Vérifier que l'utilisateur a bien été enregistré en base de données
$this->assertDatabaseHas('users', [
'email' => 'john.doe@example.com',
'name' => 'John Doe',
]);
}
/**
* Teste la validation requise pour la création d'un utilisateur.
*
* @return void
*/
public function test_it_requires_name_and_email_to_create_user(): void
{
$response = $this->postJson('/api/users', [
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertStatus(422); // 422 Unprocessable Entity (erreurs de validation)
$response->assertJsonValidationErrors(['name', 'email']); // Vérifie les erreurs pour les champs 'name' et 'email'
}
}
$this->postJson('/api/users', $userData);: Simule une requête POST avec les données fournies.$response->assertStatus(201);: Le code 201 est le standard pour une ressource créée avec succès.$this->assertDatabaseHas('users', ['email' => 'john.doe@example.com']);: Une assertion très utile qui vérifie l'existence d'un enregistrement dans la base de données avec les attributs spécifiés.
Tests PUT/PATCH : Mise à jour de ressources
Pour la mise à jour, nous aurons généralement besoin d'un utilisateur existant et des nouvelles données.
<?php
// ...
class UserApiTest extends TestCase
{
use RefreshDatabase;
/**
* Teste la mise à jour d'un utilisateur existant.
*
* @return void
*/
public function test_it_can_update_a_user(): void
{
$user = User::factory()->create(); // Créer un utilisateur existant
$updatedData = [
'name' => 'Jane Doe',
'email' => 'jane.doe@example.com',
];
$response = $this->putJson('/api/users/' . $user->id, $updatedData);
$response->assertStatus(200); // OK
$response->assertJsonFragment([
'id' => $user->id,
'name' => 'Jane Doe',
'email' => 'jane.doe@example.com',
]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Jane Doe',
'email' => 'jane.doe@example.com',
]);
}
}
Tests DELETE : Suppression de ressources
Pour la suppression, nous vérifions que la ressource n'existe plus en base de données.
<?php
// ...
class UserApiTest extends TestCase
{
use RefreshDatabase;
/**
* Teste la suppression d'un utilisateur.
*
* @return void
*/
public function test_it_can_delete_a_user(): void
{
$user = User::factory()->create(); // Créer un utilisateur à supprimer
$response = $this->deleteJson('/api/users/' . $user->id);
$response->assertStatus(204); // 204 No Content (succès, pas de contenu à retourner)
// Vérifier que l'utilisateur n'est plus en base de données
$this->assertDatabaseMissing('users', [
'id' => $user->id,
]);
}
}
Gestion de l'authentification dans les tests
Beaucoup d'API nécessitent une authentification. Laravel facilite la simulation d'un utilisateur connecté pour vos tests.
<?php
// ...
class UserApiTest extends TestCase
{
use RefreshDatabase;
/**
* Teste l'accès à un endpoint protégé par l'authentification.
*
* @return void
*/
public function test_authenticated_user_can_access_protected_endpoint(): void
{
$user = User::factory()->create(); // Créer un utilisateur
// Simuler l'authentification de cet utilisateur
$response = $this->actingAs($user)
->getJson('/api/protected-resource'); // Endpoint nécessitant authentification
$response->assertStatus(200);
// ... (autres assertions sur la réponse)
}
/**
* Teste que l'accès non authentifié à un endpoint protégé est refusé.
*
* @return void
*/
public function test_unauthenticated_user_cannot_access_protected_endpoint(): void
{
$response = $this->getJson('/api/protected-resource');
$response->assertStatus(401); // 401 Unauthorized
}
/**
* Teste l'authentification via un token API (si votre API utilise des tokens Bearer).
*
* @return void
*/
public function test_api_token_authentication(): void
{
// Supposons que votre modèle User a un champ 'api_token'
$user = User::factory()->create(['api_token' => 'my-secret-api-token']);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $user->api_token,
'Accept' => 'application/json',
])->get('/api/resource-with-token');
$response->assertStatus(200);
}
}
$this->actingAs($user): Cette méthode simule qu'un utilisateur est connecté. Toutes les requêtes suivantes dans cette méthode de test seront effectuées comme si cet utilisateur était authentifié.$this->withHeaders([...]): Permet d'ajouter des en-têtes HTTP personnalisés à la requête, utile pour les tokens d'API (Bearer tokens) ou d'autres en-têtes spécifiques.
Bonnes Pratiques et Conseils
Pour des tests efficaces et maintenables :
- Utilisez
RefreshDatabase: Toujours utiliser ce trait pour les tests d'intégration afin de garantir que chaque test démarre avec une base de données propre. - Fakers et Factories : Exploitez les factories de modèles Laravel et la librairie Faker (fournie avec Laravel) pour générer des données de test réalistes et variées. Cela réduit le code de configuration des tests et rend vos tests plus flexibles.
- Tests indépendants et atomiques : Chaque méthode de test doit tester une seule fonctionnalité et être complètement indépendante des autres. L'ordre d'exécution des tests ne doit pas avoir d'importance.
- Nommage clair et explicite : Nommez vos méthodes de test de manière descriptive, par exemple
test_it_can_create_a_user(),test_user_cannot_be_be_created_without_name(). Cela facilite la compréhension et le débogage. - Test-Driven Development (TDD) : Envisagez d'écrire vos tests avant d'implémenter la fonctionnalité. Cela force une réflexion sur l'API et la logique métier, et garantit que votre code est testable dès le départ.
- Asserts spécifiques aux réponses JSON : Laravel offre une riche panoplie d'assertions pour les réponses JSON (
assertJson,assertJsonFragment,assertJsonStructure,assertJsonCount,assertJsonMissing, etc.). Utilisez-les judicieusement pour valider précisément le contenu de votre API.
Exécuter les Tests
Pour exécuter vos tests, utilisez la commande Artisan :
php artisan test: Exécute tous les tests unitaires et de fonctionnalité.php artisan test --filter UserApiTest: Exécute uniquement les tests contenus dans la classeUserApiTest.php artisan test --parallel: (Depuis Laravel 8.x) Exécute les tests en parallèle sur plusieurs processus, accélérant considérablement le temps d'exécution des suites de tests volumineuses.
Conclusion
Les tests automatisés d'API avec PHPUnit et Laravel HTTP Tests sont un investissement essentiel pour tout projet backend sérieux. Ils garantissent non seulement la qualité et la robustesse de vos API, mais facilitent également la maintenabilité et l'évolution de votre codebase sur le long terme.
En maîtrisant la simulation des requêtes HTTP, l'interaction avec la base de données de test et les assertions spécifiques aux réponses JSON, vous acquérez la confiance nécessaire pour développer et déployer des API fiables et performantes.
Continuez à explorer la documentation de Laravel et de PHPUnit pour découvrir des techniques plus avancées comme le mocking, l'utilisation de tests de base de données plus complexes, ou l'intégration des tests dans un pipeline d'intégration continue (CI/CD). La pratique régulière et une approche disciplinée des tests vous transformeront en un développeur backend plus efficace et plus confiant.