Aller au contenu principal

Module 17 – Tests

Niveau 4 – Laravel avancé (PRO)


Objectifs

Au programme :

  • Écrire des tests unitaires (logique isolée, sans BDD ni HTTP)
  • Écrire des tests feature (requêtes HTTP, BDD, stack Laravel)
  • Utiliser PHPUnit (intégré à Laravel) ou Pest (syntaxe plus concise)
  • Mocker des dépendances (facades, services) pour isoler le code testé
  • Passer l’examen : écrire des tests obligatoires pour une fonctionnalité donnée

Théorie

Pourquoi tester ?

  • Régression : s’assurer que les changements ne cassent pas l’existant.
  • Documentation : les tests décrivent le comportement attendu.
  • Refactoring : modifier le code en confiance.
  • Livraison : une suite de tests verte est un critère de qualité avant déploiement.

Laravel est livré avec PHPUnit ; les tests sont dans tests/ (Unit et Feature).


Lancer les tests

php artisan test          # ou : ./vendor/bin/phpunit
php artisan test --filter NomDuTest
php artisan test tests/Unit
php artisan test tests/Feature/ArticleTest.php

Tests unitaires (Unit)

Isolement d’une classe ou d’une fonction : pas de requête HTTP, pas de BDD réelle (ou BDD de test). On teste la logique pure.

Exemple (tests/Unit/CalculatorTest.php) :

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
public function test_add_returns_sum(): void
{
$calc = new Calculator();
$this->assertSame(5, $calc->add(2, 3));
}
}

Assertions courantes :
assertSame($expected, $actual), assertTrue(), assertFalse(), assertEmpty(), assertCount(), assertArrayHasKey(), assertInstanceOf(), assertStringContainsString().


Tests feature (Feature)

On envoie des requêtes HTTP (GET, POST, etc.) vers l’application et on vérifie la réponse (statut, contenu, BDD). Laravel fournit des helpers : $this->get(), $this->post(), $this->actingAs($user).

Exemple (tests/Feature/ArticleTest.php) :

namespace Tests\Feature;

use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ArticleTest extends TestCase
{
use RefreshDatabase; // réinitialise la BDD avant chaque test

public function test_index_returns_articles(): void
{
Article::factory()->count(3)->create();
$response = $this->get(route('articles.index'));
$response->assertOk();
$response->assertViewHas('articles');
$this->assertCount(3, $response->viewData('articles'));
}

public function test_store_creates_article_when_authenticated(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('articles.store'), [
'title' => 'My title',
'body' => 'Contenu',
]);
$response->assertRedirect();
$this->assertDatabaseHas('articles', ['title' => 'My title']);
}

public function test_store_requires_authentication(): void
{
$response = $this->post(route('articles.store'), [
'title' => 'Title',
'body' => 'Body',
]);
$response->assertRedirect(route('login'));
}
}

Assertions HTTP :
assertOk(), assertStatus(201), assertRedirect(), assertRedirect(route('name')), assertJson(), assertViewHas(), assertSessionHas(), assertDatabaseHas(), assertDatabaseMissing().


Base de données de test

  • RefreshDatabase : exécute les migrations avant chaque test et nettoie les données (transaction ou rollback). La BDD de test est définie dans phpunit.xml (souvent SQLite en mémoire : DB_CONNECTION=sqlite, DB_DATABASE=:memory:).
  • DatabaseMigrations : exécute les migrations sans rollback entre les tests (plus lent).
  • Utiliser les factories pour créer des modèles en test : User::factory()->create(), Article::factory()->count(5)->create().

Pest (alternative à PHPUnit)

Pest est une couche au-dessus de PHPUnit avec une syntaxe plus lisible.

// tests/Feature/ArticleTest.php
use App\Models\Article;
use App\Models\User;

beforeEach(function () {
$this->user = User::factory()->create();
});

it('lists articles on index', function () {
Article::factory()->count(3)->create();
$response = $this->get(route('articles.index'));
$response->assertOk();
expect($response->viewData('articles'))->toHaveCount(3);
});

it('creates article when authenticated', function () {
$response = $this->actingAs($this->user)->post(route('articles.store'), [
'title' => 'Title',
'body' => 'Body',
]);
$response->assertRedirect();
$this->assertDatabaseHas('articles', ['title' => 'Title']);
});

Installation : composer require pestphp/pest --dev puis php artisan pest:install.


Mocking

Pour isoler le code testé, on remplace une dépendance (service, facade) par un mock qui renvoie des valeurs contrôlées.

Exemple : mock d’un service d’envoi d’email

use App\Services\Mailer;

$mailer = Mockery::mock(Mailer::class);
$mailer->shouldReceive('send')->once()->with('user@test.com', 'Sujet', 'Body')->andReturn(true);
$this->app->instance(Mailer::class, $mailer);

Facades : Laravel permet de « fake » des facades (ex. Mail::fake(), Event::fake()) pour ne pas envoyer de vrais emails ni déclencher de vrais listeners pendant les tests, et pour faire des assertions (Mail::assertSent(...)).

Mail::fake();
// ... exécuter une action qui envoie un mail
Mail::assertSent(OrderShipped::class, function ($mail) {
return $mail->order->id === 1;
});

Exemple : test de validation

public function test_store_validates_required_fields(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('articles.store'), []);
$response->assertSessionHasErrors(['title', 'body']);
$response->assertRedirect();
$this->assertDatabaseCount('articles', 0);
}

Bonnes pratiques

  1. RefreshDatabase dans les tests feature pour ne pas polluer la BDD.
  2. Factories pour créer des données réalistes et réutilisables.
  3. Un comportement par test : un test = une assertion principale (nom du test explicite).
  4. Tester les cas limites : validation en échec, non authentifié, 404, etc.
  5. Mock seulement quand nécessaire (ex. appel API externe, envoi d’email).

Quiz – Module 17

Q1. Quelle est la différence entre un test unit et un test feature ?
Q2. À quoi sert le trait RefreshDatabase ?
Q3. Comment simuler un utilisateur connecté dans un test feature ?
Q4. Comment vérifier qu’un enregistrement a bien été créé en BDD après une action ?
Q5. À quoi sert le mocking dans les tests ?

Réponses

R1. Unit : teste une unité de code isolée (classe, fonction), sans HTTP ni BDD. Feature : teste une requête HTTP complète (route → contrôleur → BDD → réponse).

R2. À réinitialiser la base avant (ou après) chaque test (migrations + données vides) pour avoir un état reproductible.

R3. Avec $this->actingAs($user) avant d’appeler get/post ; Laravel considère alors l’utilisateur comme connecté.

R4. Avec $this->assertDatabaseHas('table', ['column' => 'value']) (ou assertDatabaseMissing, assertDatabaseCount).

R5. À remplacer une dépendance (service, API externe, facade) par un double qui renvoie des valeurs contrôlées, pour isoler le code testé et éviter les effets de bord (vrais envois d’email, vrais appels HTTP).


Examen : écrire des tests obligatoires

L’examen peut consister à :

  • Écrire 2 à 3 tests feature pour une ressource (ex. articles) : index retourne 200 et des données, store crée un enregistrement quand authentifié, store renvoie des erreurs de validation si champs manquants.
  • Utiliser RefreshDatabase, factories, actingAs, assertDatabaseHas / assertSessionHasErrors.
  • (Bonus) Un test unitaire pour une classe de service ou une règle de validation personnalisée.

Critères de réussite : Les tests sont exécutables (php artisan test), verts, et couvrent les cas demandés.


Suite

Module 18 – Sécurité Laravel (CSRF, XSS, hashing, rate limiting, policies avancées).