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.
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().
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 fichierresources/js/pages/projects/Index.vueexiste.has('projects', 2)vérifie que la propprojectsexiste et 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.
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 :
- la liste des projets, avec
component()ethas(); - la fiche d'un projet, avec
where()sur son titre ; - un invité renvoyé vers
/logindepuis/admin/projets; - la création d'un projet par l'auteur connecté, avec la redirection, le flash et la ligne en base ;
- 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 groupeadmindansroutes/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.phpappliqueRefreshDatabaseà tous les tests deFeature, et chaque test crée ses données avec les fabriques.assertInertia()vérifie la page rendue aveccomponent(), et ses props avechas()etwhere().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, etassertRedirect('/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.