Les bases 6/6 : Les tests

Publié le 9 novembre 2022 par William Suppo
Couverture de l'article Les bases 6/6 : Les tests

C’est parti pour ce dernier chapitre qui concerne les tests au sein d’un projet Laravel. Avant de partir sur l’élaboration de ces derniers, on va passer en revue quelques principes et définitions.

Sommaire

  1. Création du modèle
  2. Création du contrôleur
  3. Création des vues
  4. Validation des données
  5. Contrôle d’accès
  6. Les tests

À quoi servent-ils ?

Les tests automatisés ont trois grands principes.

Leur premier rôle est de garantir que l’application fonctionne, on va pour cela viser un taux de couverture large tout en restant pragmatique.

On invoque assez souvent la loi de Pareto qui définit qu’on peut atteindre 80 % de l’objectif avec 20 % d’efforts. Ces 80 % peuvent être l’objectif du taux de couverture de tests.

Le second rôle des tests est de prévenir le dysfonctionnement de l’application après un changement dans la base de code, ce qu’on appelle une régression.

Enfin, les tests sont une mine d’or d’informations sur le comportement attendu de votre application, idéale pour les transmettre à nouveau développeur qui intègre un projet.

La pyramide des tests

Il existe différents types de tests généralement illustrés par cette pyramide des tests initié par Mike Cohn :

Untitled

On retrouve à la base les tests unitaires. Ces derniers visent à vérifier le bon fonctionnement d’une fonction ou une méthode en dehors de tout contexte. C’est-à-dire qu’importe ce qui gravite autour de la méthode dans l’application, si on lui passe toujours les mêmes paramètres elle retournera le même résultat.

Ensuite viennent les tests fonctionnels. Là, on prend un peu de recul, on va tester une fonctionnalité dans laquelle on va apporter du contexte :

En haut de la pyramide, on retrouve les tests d’acceptances, ou tests de bout en bout. On prend encore plus de recul, tellement qu’on imagine notre application comme une boite noire et on va interagir avec elle pour vérifier que son comportement est bien celui attendu.

A noter qu’il est tout à fait possible d’automatiser les tests de bout en bout avec des outils comme Dusk.

Les tests unitaires

Maintenant, qu’on a introduit la notion de tests, passons à l’élaboration de nos premiers dans l’application.

Quand, j’élabore des tests, je me concentre en premier sur l’aspect sécurité en vérifiant que les utilisateurs ne peuvent déroger aux droits d’accès pensé dans l’application.

Nous avons ajouté au sein du modèle App\Models\User des méthodes qui permettent de vérifier le rôle de l’utilisateur, testons leur bon fonctionnement dans un nouveau fichier de test tests/Unit/UserTest.php que nous allons décortiquer :

1namespace Tests\Unit;
2 
3use Tests\TestCase;
4 
5class UserTest extends TestCase
6{
7 /** ... */
8}

Voilà l’ossature de notre fichier, on note qu’il existe, par défaut, un namespace dédié aux tests unitaires.

Aussi, afin d’exploiter le panel de fonctions qu’offre Laravel pour les tests, on hérite notre classe de Tests\TestCase.

Passons au premier test :

1/** @test */
2public function can_be_admin()
3{
4 $user = User::factory()->make(['role' => 'user']);
5 $this->assertFalse($user->isAdmin());
6 
7 $owner = User::factory()->make(['role' => 'owner']);
8 $this->assertFalse($owner->isAdmin());
9 
10 $admin = User::factory()->make(['role' => 'admin']);
11 $this->assertTrue($admin->isAdmin());
12}

Tout d’abord, notons le nom de la méthode, elle permet de contextualiser l’objet du test : que veut-on vérifier ? On veut vérifier qu’un utilisateur peut être admin.

On instancie ici plusieurs modèles User avec pour chacun un rôle distinct afin de vérifier comment réagit la méthode isAdmin pour chacun d’entre eux. On s’attend bien évidemment à ce que seul l’appel à cette méthode pour utilisateur ayant le rôle admin retourne vrai.

On en fait de même avec la méthode isOwner dans un nouveau test :

1/** @test */
2public function can_be_owner()
3{
4 $user = User::factory()->make(['role' => 'user']);
5 $this->assertFalse($user->isOwner());
6 
7 $owner = User::factory()->make(['role' => 'owner']);
8 $this->assertTrue($owner->isOwner());
9 
10 $admin = User::factory()->make(['role' => 'admin']);
11 $this->assertFalse($admin->isOwner());
12}

Les tests fonctionnels

Passons aux tests fonctionnels, le but pour chacun d’entre eux va être de vérifier le comportement de la fonctionnalité testée avec différents contextes. Là où le contexte va varier, c’est sur le rôle de l’utilisateur.

Lorsque j’élabore des tests, je me concentre en premier sur le cas d’usage, c’est-à-dire le contexte dans lequel la fonctionnalité a été pensée. Ensuite, je verrouille en vérifiant que seul les ayant droits peuvent y accéder. Enfin, je peaufine si nécessaire avec des tests qui valide l’expérience utilisateur, ça peut être vérifier qu’un message est bien renvoyé à l’utilisateur lorsqu’il se trompe dans son formulaire par exemple.

Afin de faciliter la lecture des tests, ces derniers ont été séparés par fonctionnalité :

On commence avec la lecture de nos restaurants :

1namespace Tests\Feature;
2 
3use App\Models\Restaurant;
4use App\Models\User;
5use Illuminate\Foundation\Testing\RefreshDatabase;
6use Tests\TestCase;
7 
8class ShowRestaurantTest extends TestCase
9{
10 use RefreshDatabase;
11 
12 /** @test */
13 public function guest_can_view_restaurant_list()
14 {
15 $owner = User::factory()->create(['role' => 'owner']);
16 Restaurant::factory()
17 ->for($owner)
18 ->count(3)
19 ->create();
20 
21 $response = $this->get('/restaurants');
22 
23 $response->assertSuccessful();
24 
25 $this->assertCount(3, $response['restaurants']);
26 $this->assertContainsOnlyInstancesOf(Restaurant::class, $response['restaurants']);
27 }
28 
29 /** ... */
30}

Tout d’abord, un point sur le RefreshDatabase : il permet de remettre à zéro la base de données entre chaque test, utile pour éviter les effets de bord d’un test à l’autre.

Ensuite le nom du test, encore une fois, apporte le plus de contexte possible en répondant à la question : qui veut faire quoi ?

Dans notre cas : Un visiteur peut voir la liste des restaurants.

Maintenant détaillons le contenu de celui-ci :

1$owner = User::factory()->create(['role' => 'owner']);
2Restaurant::factory()
3 ->for($owner)
4 ->count(3)
5 ->create();

En premier, on crée les éléments utiles à contextualiser notre test, ici 3 restaurants rattachés à un propriétaire.

1$response = $this->get('/restaurants');

Ici, on appelle la fonctionnalité testée, concrètement ce qui se passe, c’est que Laravel va simuler un appel à l’url /restaurants en utilisant le verbe GET et exécuter le code comme si c’était le cas jusqu’à rendre un objet Response.

1$response->assertSuccessful();
2 
3$this->assertCount(3, $response['restaurants']);
4$this->assertContainsOnlyInstancesOf(Restaurant::class, $response['restaurants']);

Enfin, on contrôle le résultat, en commençant par vérifier que notre réponse est un succès, code 200. Puis en vérifiant que notre vue contient bien 3 objets de type Restaurant.

On peut être encore plus fin en vérifiant que ce sont bien les 3 restaurants créés qui sont contenus dans notre vue et même aller jusqu’à vérifier le contenu de notre vue avec les méthodes tel que assertSee.

Prochain test abordé, celui de la fonction de création d’un restaurant où, petit rappel, seul un propriétaire peut l’utiliser.

Dans celui-cin on va vérifier 2 choses : que le futur propriétaire puisse accéder au formulaire et aussi que celui-ci puisse le soumettre. Voici le corps de la fonction :

1/** @test */
2public function owner_can_create_restaurant()
3{
4 $owner = User::factory()->create(['role' => 'owner']);
5 
6 $response = $this->actingAs($owner)->get('/restaurants/create');
7 $response->assertSuccessful();
8 
9 $expectedAttributes = [
10 'name' => 'O spaghetti',
11 'type' => 'Italien',
12 'address' => 'Petite place du centre ville',
13 ];
14 
15 $response = $this->actingAs($owner)->post('/restaurants', $expectedAttributes);
16 $response->assertRedirect('/restaurants/1');
17 
18 $expectedAttributes['user_id'] = $owner->id;
19 $this->assertDatabaseHas(Restaurant::class, $expectedAttributes);
20}

Comme précédemment, on commence par créer nos éléments, ici le futur propriétaire :

1$owner = User::factory()->create(['role' => 'owner']);

Ensuite, on vérifie que ce dernier peut accéder au formulaire, c’est-à-dire à la page /restaurants/create. Via Laravel, on peut spécifier avec quel utilisateur on souhaite aller sur la page via la méthode actingAs :

1$response = $this->actingAs($owner)->get('/restaurants/create');
2$response->assertSuccessful();

On continue, avec la soumission du formulaire pour laquelle on prépare au préalable les champs envoyés dans la variable $expectedAttributes :

1$expectedAttributes = [
2 'name' => 'O spaghetti',
3 'type' => 'Italien',
4 'address' => 'Petite place du centre ville',
5];
6 
7$response = $this->actingAs($owner)->post('/restaurants', $expectedAttributes);

Enfin, on vérifie que le propriétaire est redirigé vers la page du restaurant nouvellement créé puis que la base de données contient bien les informations du restaurant liées à son propriétaire :

1$response->assertRedirect('/restaurants/1');
2 
3$expectedAttributes['user_id'] = $owner->id;
4$this->assertDatabaseHas(Restaurant::class, $expectedAttributes);

Dernier test passé en revue, celui du cas où un simple utilisateur veut tenter de créer un restaurant :

1/** @test */
2public function user_cant_create_restaurant()
3{
4 $user = User::factory()->create(['role' => 'user']);
5 
6 $response = $this->actingAs($user)->get('/restaurants/create');
7 $response->assertForbidden();
8 
9 $response = $this->actingAs($user)->post('/restaurants', []);
10 $response->assertForbidden();
11 
12 $this->assertDatabaseCount(Restaurant::class, 0);
13}

Le processus est le même que pour le cas normal, si ce n’est qu’on va vérifier que la porte reste bien fermer pour notre utilisateur avec le code :

1$response->assertForbidden();

Et que notre base ne contient pas d’informations :

1$this->assertDatabaseCount(Restaurant::class, 0);

Conclusion

Avec cette suite de tutoriels, vous devriez avoir de quoi développer vos premières fonctionnalités au sein d’un projet Laravel. Pour aller plus loin, nous vous invitons à consulter le Bootcamp de Laravel et à vous renseigner sur les systèmes d’authentification des utilisateurs.

Source : https://github.com/laravel-fr/support-les-bases/tree/v6
William Suppo avatar
William Suppo
Je mange du PHP au p'tit dej', et vous ?

A lire

Autres articles de la même catégorie