Aller au contenu

Les premiers tests fonctionnels

Temps 1 · séance 4 · ~55 min

Les slides du chapitre (PDF)PDF

  • Écrire un test Pest qui visite une page du portfolio et vérifie sa réponse.
  • Vérifier le composant et les props d'une page Inertia avec assertInertia.
  • Tester la protection du back-office, pour un invité et pour l'auteur connecté.
  • Tester le formulaire de création avec des données valides et invalides.
  • Lire un test rouge : ce qui était attendu, ce qui a été obtenu.

1. Pest, en rappel

En 5XCOS, vous avez écrit des tests Pest pour vos katas et pour le refuge. Un test est une fonction test() qui reçoit une phrase et une fonction. La phrase décrit le comportement attendu, et la fonction le vérifie. Si une vérification échoue, le test est rouge.

Le kit est livré avec ses propres tests, dans tests/Feature/Auth, tests/Feature/Settings et quelques autres fichiers. Lancez-les tous :

php artisan test

Un test fonctionnel envoie une vraie requête à votre application, sans navigateur, et vérifie la réponse : le code HTTP, la redirection, la page rendue, les données enregistrées. Il vit dans tests/Feature. Un test unitaire, dans tests/Unit, vérifie une classe seule, sans requête.

Le test envoie une requête à votre application, sans navigateur, et lit la réponse. Il vérifie le code HTTP avec assertOk(), la redirection avec assertRedirect('/login'), la page rendue avec assertInertia() et les données enregistrées avec expect()

Aujourd'hui, vous écrivez les vôtres dans un seul fichier, tests/Feature/PortfolioTest.php. Créez-le avec ces trois lignes en tête :

<?php

use App\Models\Project;
use App\Models\User;

2. Une base vide à chaque test

Ouvrez tests/Pest.php. Le kit y a écrit ces lignes :

pest()->extend(TestCase::class)
    ->use(RefreshDatabase::class)
    ->in('Feature');

Chaque test du dossier Feature démarre donc l'application de Laravel, grâce à TestCase, et utilise RefreshDatabase. Ce trait crée les tables avant chaque test et les vide après. Le fichier phpunit.xml du kit fait le reste : DB_DATABASE y vaut :memory:, une base SQLite qui n'existe que dans la mémoire, le temps des tests.

Vos trois vrais projets ne sont jamais touchés, et chaque test commence avec une base vide. C'est à lui de créer ce dont il a besoin. Les fabriques du chapitre 1.06 servent à cela :

Project::factory()->count(2)->create();

Cette ligne enregistre deux projets aux titres inventés. La fabrique les publie : published_at vaut now() dans sa méthode definition(). Pour un brouillon, vous écririez Project::factory()->draft()->create().

Avec RefreshDatabase, chaque test part d’une base vide en mémoire, :memory:, y crée deux projets avec Project::factory(), puis la base est vidée ; le test suivant repart d’une base vide. Vos trois vrais projets ne sont jamais touchés

3. Une première page : assertOk() et assertSee()

Le test le plus simple visite une adresse et vérifie que la réponse est bonne :

test('the projects page shows a title', function () {
    Project::factory()->create(['title' => 'Quiz des capitales']);

    $this->get('/projets')
        ->assertOk()
        ->assertSee('Quiz des capitales');
});

$this->get('/projets') envoie une requête GET à l'application et renvoie la réponse. assertOk() vérifie le code 200. assertSee() cherche un texte dans le HTML de la réponse. Ce test passe.

Écrivez le même test avec le titre « Météo Wavre » :

test('the projects page shows an accented title', function () {
    Project::factory()->create(['title' => 'Météo Wavre']);

    $this->get('/projets')
        ->assertOk()
        ->assertSee('Météo Wavre');
});

Il échoue. Pest affiche tout le HTML de la réponse, puis ce qu'il cherchait. En voici la fin, coupée :

  ⨯ the projects page shows an accented title
  …
        <script data-page="app" type="application/json">{"component":"projects\/Index","props":{…,"projects":[{"title":"Météo Wavre",…}]},…}</script><div id="app"></div>\n
    </body>\n
</html>\n

  To contain: Météo Wavre

La page HTML ne contient pas votre liste. Elle contient une balise <script data-page> avec les props en JSON, et une div vide que Vue remplira dans le navigateur. Vous l'avez vu au chapitre 1.07. Dans ce JSON, le é est écrit \u00e9, et assertSee() ne reconnaît plus le titre.

assertSee() cherche une chaîne n'importe où dans la réponse, sans savoir où elle devrait être. Avec Inertia, il existe mieux : vérifier directement la page et ses props. Supprimez ces deux tests.

4. La page et ses props : assertInertia()

Inertia fournit une méthode de test qui lit le nom du composant et les props envoyés à la page. Ajoutez cet import en tête du fichier :

use Inertia\Testing\AssertableInertia as Assert;

Puis le premier vrai test :

test('the projects page lists the published projects', function () {
    Project::factory()->count(2)->create();

    $this->get('/projets')
        ->assertOk()
        ->assertInertia(fn (Assert $page) => $page
            ->component('projects/Index')
            ->has('projects', 2)
        );
});

assertInertia() reçoit une fonction fléchée. Cette fonction reçoit la page, un objet Assert, et enchaîne les vérifications.

  • component('projects/Index') vérifie le nom de la page passé à Inertia::render(). Il vérifie aussi que le fichier resources/js/pages/projects/Index.vue existe.
  • has('projects', 2) vérifie que la prop projects existe et contient deux éléments.

assertInertia() lit la page rendue : component('projects/Index') compare le nom du composant et vérifie que le fichier resources/js/pages/projects/Index.vue existe ; has('projects', 2) vérifie que la prop projects contient deux éléments

Le test de la fiche vérifie une valeur précise avec where() :

test('a project page shows its title', function () {
    Project::factory()->create(['title' => 'Météo Wavre', 'slug' => 'meteo-wavre']);

    $this->get('/projets/meteo-wavre')
        ->assertOk()
        ->assertInertia(fn (Assert $page) => $page
            ->component('projects/Show')
            ->where('project.title', 'Météo Wavre')
        );
});

where('project.title', 'Météo Wavre') lit la clé title de la prop project. Le point descend d'un niveau dans les props, comme dans toast.message plus loin. La comparaison porte sur la valeur elle-même, avant tout encodage : l'accent ne gêne plus.

Ce test fixe le slug lui-même, parce que l'adresse visitée en dépend. La fabrique en aurait inventé un autre.

5. Lire un test rouge

Un test rouge dit ce qu'il attendait et ce qu'il a obtenu. Écrivez 'Projects/Index', avec une majuscule, dans le premier test, et lancez seulement ce fichier :

php artisan test tests/Feature/PortfolioTest.php
   FAIL  Tests\Feature\PortfolioTest
  ⨯ the projects page lists the published projects
  ───────────────────────────────────────────────
   FAILED  Tests\Feature\PortfolioTest > the projects page lists the published projects
  Unexpected Inertia page component.
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'Projects/Index'
+'projects/Index'
  at vendor\inertiajs\inertia-laravel\src\Testing\AssertableInertia.php:131

Lisez-le de haut en bas. Le nom du test rouge vient d'abord. Puis le message : le composant de la page n'est pas celui attendu. Les lignes - et + comparent les deux chaînes, Expected pour ce que vous avez écrit, Actual pour ce que l'application a rendu. La seule différence est la majuscule.

Un test rouge compare deux chaînes : Expected, ce que votre test a écrit, 'Projects/Index' avec une majuscule, et Actual, ce que l’application a rendu, 'projects/Index'. Le test échoue, FAIL. Une fois la minuscule remise, component('projects/Index') passe, PASS. Sous Windows, Projects et projects désignent le même dossier, mais pas sur le serveur Linux d’OVH

Remettez la minuscule. La différence compte aussi en dehors des tests : sous Windows, Projects et projects désignent le même dossier, mais pas sur le serveur Linux d'OVH.

6. La protection du back-office

Un invité qui ouvre /admin/projets doit être renvoyé vers la page de connexion. Un test le vérifie en une ligne :

test('guests cannot open the admin', function () {
    $this->get('/admin/projets')->assertRedirect('/login');
});

assertRedirect('/login') vérifie que la réponse est une redirection, et qu'elle mène à cette adresse. C'est le middleware auth du groupe admin, posé au chapitre 1.10, qui la produit.

Pour l'auteur, il faut un utilisateur connecté. actingAs($user) fait comme si cet utilisateur s'était connecté, pour les requêtes qui suivent dans le test. La fabrique du kit crée un utilisateur à l'adresse e-mail vérifiée :

$this->actingAs(User::factory()->create())
    ->get('/admin/projets')
    ->assertOk();

Le test de la section suivante s'en sert pour créer un projet.

7. Le formulaire de création

Des données valides

test('the author can create a project', function () {
    $this->actingAs(User::factory()->create())
        ->post('/admin/projets', [
            'title' => 'Mon portfolio',
            'summary' => 'Le site que vous lisez.',
        ])
        ->assertRedirect('/admin/projets')
        ->assertInertiaFlash('toast.message', 'Projet créé.');

    expect(Project::where('slug', 'mon-portfolio')->exists())->toBeTrue();
});

post() envoie les données comme le ferait votre <Form>. Le test vérifie trois choses. La réponse redirige vers la liste, comme to_route('admin.projects.index') dans store(). assertInertiaFlash() lit le flash Inertia::flash('toast', ...) du chapitre 1.12 et vérifie son message. Enfin, expect() interroge la base : le projet existe, avec le slug calculé à partir du titre.

Des données invalides

test('a project without title is refused', function () {
    $this->actingAs(User::factory()->create())
        ->post('/admin/projets', ['title' => '', 'summary' => 'Un résumé.'])
        ->assertSessionHasErrors('title');

    expect(Project::count())->toBe(0);
});

Au chapitre 1.12, vous avez vu qu'un envoi invalide répond par une redirection 302 vers le formulaire, avec les erreurs rangées dans la session. Inertia les y lit pour remplir errors. assertSessionHasErrors('title') vérifie qu'une erreur existe pour le champ title. La dernière ligne vérifie que rien n'a été enregistré.

8. Pratique : cinq tests verts (25 min)

Rassemblez dans tests/Feature/PortfolioTest.php les cinq tests des sections 4, 6 et 7, adaptés à votre portfolio :

  1. la liste des projets, avec component() et has() ;
  2. la fiche d'un projet, avec where() sur son titre ;
  3. un invité renvoyé vers /login depuis /admin/projets ;
  4. la création d'un projet par l'auteur connecté, avec la redirection, le flash et la ligne en base ;
  5. la création refusée sans titre, avec assertSessionHasErrors().

Lancez le fichier :

php artisan test tests/Feature/PortfolioTest.php
   PASS  Tests\Feature\PortfolioTest
  ✓ the projects page lists the published projects                       0.55s
  ✓ a project page shows its title                                       0.03s
  ✓ guests cannot open the admin                                         0.03s
  ✓ the author can create a project                                      0.05s
  ✓ a project without title is refused                                   0.03s

  Tests:    5 passed (30 assertions)
  Duration: 0.93s

Les durées changent d'une machine à l'autre. Lancez ensuite php artisan test sans argument : les tests du kit restent verts eux aussi.

Pour vérifier que chaque test sert à quelque chose, cassez exprès le code qu'il protège, un test à la fois, puis réparez.

  • Retirez 'auth' du groupe admin dans routes/web.php : le test de l'invité passe au rouge, car l'invité n'est plus renvoyé vers /login.
  • Changez le message du flash dans store() : le test de création passe au rouge.

Bonus, pour qui a fini : un sixième test pour le brouillon du chapitre 1.16. Créez un projet avec Project::factory()->draft()->create(['slug' => 'pas-fini']), visitez /projets/pas-fini, et vérifiez la 404 avec assertNotFound().

Ce que votre portfolio contient maintenant

Cinq tests verts dans tests/Feature/PortfolioTest.php. Ils vérifient la liste et la fiche publiques, la protection du back-office, la création d'un projet et le refus d'un projet sans titre. Si une modification future casse l'un de ces comportements, php artisan test le dit avant vos visiteurs.

À retenir

  • tests/Pest.php applique RefreshDatabase à tous les tests de Feature, et chaque test crée ses données avec les fabriques.
  • assertInertia() vérifie la page rendue avec component(), et ses props avec has() et where().
  • assertSee() ne lit que le HTML brut, où les props d'Inertia sont du JSON aux accents échappés.
  • actingAs() connecte un utilisateur pour le test, et assertRedirect('/login') vérifie qu'un invité est renvoyé.
  • Un envoi invalide se teste avec assertSessionHasErrors(), parce qu'Inertia reçoit les erreurs par une redirection et la session.