Introduction : Pourquoi les tests en PHP ne sont pas une option, mais une norme
Dans le développement PHP moderne, les tests ont depuis longtemps cessé d'être une « bonne pratique » pour devenir une étape obligatoire des projets en production. Sans couverture de code par des tests, vous risquez de publier en production une erreur qui pourrait coûter de l'argent et de la réputation à votre entreprise. Dans cet article, nous allons aborder les trois niveaux de tests : unit (tests unitaires), integration (tests d'intégration) et e2e (tests de bout en bout). Vous découvrirez leurs différences, les outils à utiliser (PHPUnit, Pest, Laravel Dusk, Codeception) et comment élaborer une stratégie de test efficace pour votre projet PHP.
1. Tests unitaires : tester des unités de code isolées
Les tests unitaires vérifient le fonctionnement de classes, fonctions ou méthodes individuelles, isolées des dépendances externes (BD, API, système de fichiers). C'est le type de test le plus rapide et le moins coûteux. Le principal outil en PHP est PHPUnit (la norme de facto) et son alternative « tendance » Pest.
1.1. Installation et premier test avec PHPUnit
Installez PHPUnit via Composer :
composer require --dev phpunit/phpunitCréez un test pour une simple classe calculatrice :
<?php// src/Calculator.phpclass Calculator { public function add(int $a, int $b): int { return $a + $b; }}
// tests/CalculatorTest.phpuse PHPUnit\\Framework\\TestCase;
class CalculatorTest extends TestCase { public function testAddition(): void { $calc = new Calculator(); $result = $calc->add(2, 3); $this->assertEquals(5, $result); }}Lancez le test :
./vendor/bin/phpunit tests1.2. Utilisation d'objets Mock
Pour isoler la classe testée, nous « remplaçons » ses dépendances à l'aide de mocks. Exemple avec un repository d'utilisateurs :
<?phpclass UserService { private UserRepository $repo; public function __construct(UserRepository $repo) { $this->repo = $repo; } public function getUserName(int $id): string { $user = $this->repo->findById($id); return $user ? $user->getName() : 'Guest'; }}
// Dans le test$mockRepo = $this->createMock(UserRepository::class);$mockRepo->method('findById') ->with(1) ->willReturn(new User('Alice'));
$service = new UserService($mockRepo);$this->assertEquals('Alice', $service->getUserName(1));Best practice : Écrivez des tests unitaires pour toute la logique métier. Ils doivent s'exécuter en quelques secondes et ne pas dépendre de l'environnement.
2. Tests d'intégration : vérifier l'interaction avec la BD, l'API et les services externes
Les tests d'intégration vérifient comment vos classes fonctionnent ensemble avec des composants réels : base de données, cache, services HTTP externes. Ils sont plus lents que les tests unitaires, mais offrent une plus grande confiance dans le fait que le système fonctionne comme un tout cohérent.
2.1. Test de l'interaction avec la base de données (Laravel + RefreshDatabase)
Si vous utilisez Laravel, les tests d'intégration deviennent très simples grâce au trait RefreshDatabase :
<?phpnamespace Tests\\Feature;
use Tests\\TestCase;use Illuminate\\Foundation\\Testing\\RefreshDatabase;
class UserRegistrationTest extends TestCase { use RefreshDatabase;
public function test_user_can_register(): void { $response = $this->post('/register', [ 'name' => 'John', 'email' => 'john@example.com', 'password' => 'secret123', ]);
$response->assertStatus(302); // redirection après succès $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); }}Le trait RefreshDatabase migre automatiquement la BD avant les tests et