Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Cours Laravel 11 – les tests
Mercredi 20 mars 2024 14:21

Les développeurs PHP n'ont pas toujours eu l'habitude de créer des tests pour leurs applications. Cela s'explique en partie par l'histoire du langage PHP, qui a commencé comme un simple moyen de scripter dans le code HTML, avant de progressivement évoluer vers une langue de programmation plus sophistiquée. Avec le développement des frameworks, une nouvelle façon d'organiser le code PHP a été initiée, notamment en mettant en avant la séparation des tâches, ce qui a ouvert la possibilité de créer des tests.

Laravel a été conçu pour intégrer des tests, avec une infrastructure de base et des fonctions d'aide. Dans ce chapitre, nous allons explorer cet aspect de Laravel. Considérez cela comme une introduction à un domaine qui mériterait un cours complet à lui seul. Nous vous montrerons l'utilité de créer des tests, comment les préparer, et comment les isoler.

Lorsque vous développez en PHP, vous effectuez naturellement des tests, au moins de manière manuelle. Par exemple, si vous créez un formulaire, vous allez l'utiliser, entrer diverses informations, et tenter des fausses manœuvres. Imaginez si tout cela pouvait être automatisé, et que vous n'aviez qu'à cliquer sur un bouton pour lancer tous les tests. C'est l'objet de ce chapitre.

Vous pourriez aussi penser que rédiger des tests représente un travail supplémentaire, que ce n'est pas toujours facile, et que ce n'est pas nécessaire dans tous les cas. C'est à vous de décider si vous avez besoin d'en créer ou non. Pour des petites applications, la question peut rester ouverte. Cependant, dès qu'une application gagne en ampleur ou est développée par plusieurs personnes, il devient rapidement nécessaire de créer des tests automatisés pour garantir la qualité et la fiabilité de l'application.

Laravel a été conçu en tenant compte de l'importance des tests. En fait, le support pour les tests avec Pest et PHPUnit est inclus dès le départ, et un fichier phpunit.xml est déjà configuré pour votre application. Le framework est également livré avec des méthodes d'aide pratiques qui vous permettent de tester vos applications de manière expressive et efficace.

Laravel intègre donc des fonctionnalités qui facilitent la création de tests unitaires, fonctionnels ou d'intégration. Ces tests peuvent être utilisés pour vérifier le fonctionnement correct des différentes parties de votre application, comme les routes, les contrôleurs, les modèles et même les requêtes et les vues.

Pest et PHPUnit sont des bibliothèques de test très populaires dans le monde du développement PHP. Pest est un framework de test plus récent, conçu pour être simple et efficace, tandis que PHPUnit est un framework de test bien établi et plus complet. Laravel supporte les deux, ce qui vous permet de choisir celui qui vous convient le mieux.

Grâce aux outils de test intégrés et aux méthodes d'aide fournies par Laravel, vous pouvez créer des tests facilement et vous assurer que votre application fonctionne correctement. Cela contribue à améliorer la qualité et la fiabilité de votre application, tout en facilitant la détection et la résolution de problèmes.

L’intendance des tests

PHPUnit

Pour cet article on va utiliser PHPUnit pour effectuer les tests (les principes restent les mêmes avec Pest). C’est un framework créé par Sebastian Bergmann qui fonctionne à partir d’assertions.

Ce framework est installé comme dépendance de Laravel en mode développement :
"require-dev": {
    ...
    "phpunit/phpunit": "^10.5"
},

Mais vous pouvez aussi utiliser le fichier phar et le placer à la racine de votre application et vous êtes prêt à tester !

Vous pouvez vérifier que ça fonctionne en entrant cette commande :
php phpunit(numéro de version).phar -h
Vous obtenez ainsi la liste de toutes les commandes disponibles. Si vous utilisez la version installée avec Laravel ça donne :
php vendor\phpunit\phpunit\phpunit -h

POur les tests nous utiliserons Artisan :

php artisan test

L’intendance de Laravel

Si vous regardez les dossiers de Laravel vous allez en trouver un qui est consacré aux tests :

Toutes les classes de test que vous allez créer devront étendre cette classe TestCase.

On a 2 exemples de tests déjà présents, un dans Unit\ExampleTest.php :
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_that_true_is_true(): void
    {
        $this->assertTrue(true);
    }
}
Et un autre dans Features\ExampleTest.php :
<?php

namespace Tests\Feature;

use Tests\TestCase;
// use Illuminate\Foundation\Testing\RefreshDatabase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_the_application_returns_a_successful_response(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

Pourquoi 2 dossiers ?

Dans Laravel, par défaut, le répertoire de tests de votre application contient deux dossiers: "Feature" et "Unit". Les tests unitaires se concentrent sur une portion très petite et isolée de votre code. En fait, la plupart des tests unitaires se focalisent probablement sur une seule méthode. Les tests se trouvant dans votre dossier "Unit" ne démarrent pas votre application Laravel et ne peuvent donc pas accéder à sa base de données ou à d'autres services du framework.

Les tests fonctionnels (ou tests "Feature") peuvent tester une partie plus importante de votre code, incluant l'interaction entre plusieurs objets ou même une requête HTTP complète vers une URL JSON. En général, la majorité de vos tests devraient être des tests fonctionnels. Ces types de tests vous donnent le plus de confiance sur le fonctionnement global de votre système, en tant qu'entité cohérente.

En résumé, les tests unitaires servent à valider les composants individuels de votre application, tandis que les tests fonctionnels sont conçus pour valider l'intégration et la collaboration entre ces composants. Laravel fournit des outils et une structure de dossier claire pour faciliter la création et l'organisation de ces deux types de tests, ce qui vous permet de garantir la qualité et la fiabilité de votre application.

Sans entrer pour le moment dans le code sachez simplement que dans le premier exemple on se contente de demander si un truc vrai est effectivement vrai (bon c'est sûr que ça devrait être vrai ^^). Dans le second on envoie une requête pour la route de base et on attend une réponse positive (200).

Pour lancer ces tests c’est très simple, entrez la commande php artisan test :

On voit qu’ont été effectués 2 tests et 2 assertions et que tout s’est bien passé.

L’environnement de test

Je vous ai dit que les tests s’effectuent dans un environnement particulier, ce qui est bien pratique.

Où se trouve cette configuration ?

Regardez le fichier phpunit.xml :
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>app</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_MAINTENANCE_DRIVER" value="file"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_STORE" value="array"/>
        <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
        <!-- <env name="DB_DATABASE" value=":memory:"/> -->
        <env name="MAIL_MAILER" value="array"/>
        <env name="PULSE_ENABLED" value="false"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>
On trouve déjà des variables d’environnement actives, par exemple :
  • APP_ENV : là on dit qu’on est en mode testing,
  • BCRYPT_ROUNDS : avec une valeur de 4 (par défaut c'est 10),
  • CACHE_STORE : en mode array ce qui signifie qu’on ne va rien mettre en cache pendant les tests (par défaut on a file),
  • MAIL_MAILER : en mode array donc on n'envoie pas les mails,
  • QUEUE_CONNECTION : en mode sync, donc on aura pas de file d'attente,
  • SESSION_DRIVER : en mode array ce qui signifie qu’on ne va pas faire persister la session (par défaut on a file),
  • TELESCOPE_ENABLED : ne sert que si Telescope a été installé.
Mais aussi deux non actives :
  • DB_CONNECTION : le type de connexion,
  • DB_DATABASE : la base de données,

On peut évidemment ajouter les variables dont on a besoin. Par exemple si pendant les tests je ne veux plus sqlite mais MySql.

Maintenant pour les tests je vais utiliser sqlite.

Construire un test

Les trois étapes d’un test

Pour construire un test on procède généralement en trois étapes :

  1. on initialise les données,
  2. on agit sur ces données,
  3. on vérifie que le résultat est conforme à notre attente.

Comme tout ça est un peu abstrait prenons un exemple. Remplacez le code avec celui-ci (peu importe dans quel dossier) :

public function testBasicTest(): void
{
    $data = [10, 20, 30];
    $result = array_sum($data);
    $this->assertEquals(60, $result);
}

Supprimez le test dans l'autre dossier pour éviter de polluer les résultats.

On trouve nos trois étapes. On initialise les données :
$data = [10, 20, 30];
On agit sur ces données :
$result = array_sum($data);
On teste le résultat :
$this->assertEquals(60, $result);
La méthode assertEquals permet de comparer deux valeurs, ici 60 et $result. Si vous lancez le test vous obtenez : Vous voyez à nouveau l’exécution d’un test. Le tout s’est bien passé. Changez la valeur 60 par une autre et vous obtiendrez ceci :

Vous connaissez maintenant le principe de base d’un test et ce qu’on peut obtenir comme renseignement en cas d’échec.

Assertions et appel de routes

Les assertions

Les assertions constituent l’outil de base des tests. On en a vu une ci-dessus et il en existe bien d’autres. Vous pouvez en trouver la liste complète ici.

Voici quelques assertions et l’utilisation d’un helper de Laravel que l’on teste au passage :

public function testBasicTest(): void
{
    $data = 'Je suis petit';
    $this->assertTrue(str()->startsWith($data, 'Je'));
    $this->assertFalse(str()->startsWith($data, 'Tu'));
    $this->assertSame(str()->startsWith($data, 'Tu'), false);
    $this->assertStringStartsWith('Je', $data);
    $this->assertStringEndsWith('petit', $data);
}
Lorsqu’on lance le test on obtient ici : Tout se passe bien...

Appel de route et test de réponse

Il est facile d’appeler une route pour effectuer un test sur la réponse. Modifiez la route de base pour celle-ci :

Route::get('/', function () {
    return 'coucou';
});

On a donc une requête avec l’url de base et comme réponse la chaîne coucou. Nous allons tester que la requête aboutit bien, qu’il y a une réponse correcte et que la réponse est coucou (effectuez ce test dans le dossier Feature) :

public function testBasicTest(): void
{
    $response = $this->get('/');
    $response->assertSuccessful();
    $this->assertEquals('coucou', $response->getContent());
}

L’assertion assertSuccessful nous assure que la réponse est correcte. Ce n’est pas une assertion de PHPUnit mais une spécifique de Laravel. Vous trouvez toutes les assertion de Laravel ici.

La méthode getContent permet de lire la réponse. Le test est à nouveau correct.

Les vues et les contrôleurs

Les vues

Qu’en est-il si on retourne une vue ?

Mettez ce code pour la route :
Route::get('/', function () {
    return view('welcome')->with('message', 'Vous y êtes !');
});
Ajoutez dans cette vue ceci
{{ $message }}
Maintenant voici le test :
public function testBasicTest(): void
{
    $response = $this->get('/');
    $response->assertViewHas('message', 'Vous y êtes !');
}

On envoie la requête et on récupère la réponse. On peut tester la valeur de la variable $message dans la vue avec l'assertion assertViewHas. A nouveau le test est correct.

Maintenant changer le code ainsi :
$response->assertViewHas('message', 'Vous n\'y êtes pas !');
Cette fois le test donne une erreur : Et vous voyez la précision du commentaire.

Les contrôleurs

Créez ce contrôleur :
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class WelcomeController extends Controller
{
    public function index()
    {
        return view('welcome');
    }
}
Créez cette route pour mettre en œuvre le contrôleur ci-dessus :
use App\Http\Controllers\WelcomeController;

Route::get('welcome', [ WelcomeController::class, 'index']);

Vérifiez que ça fonctionne (vous aurez peut-être besoin de retoucher la vue où nous avons introduit une variable).

Supprimez le fichier ExampleTest.php qui ne va plus nous servir.

Créez ce test avec Artisan :
php artisan make:test WelcomeControllerTest
Par défaut vous obtenez ce code :
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class WelcomeControllerTest extends TestCase
{
    /**
     * A basic feature test example.
     */
    public function test_example(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}
Changez ainsi le code de la méthode :
public function test_example(): void
{
    $response = $this->get('welcome');
    $response->assertStatus(200);
}
Là encore le test est bon.

Isoler les tests

Nous allons maintenant aborder un aspect important des tests qui ne s’appellent pas unitaires pour rien.

Pour faire des tests efficaces il faut bien les isoler, donc savoir ce qu’on teste, ne tester qu’une chose à la fois et ne pas mélanger les choses.

Ceci est possible si le code est bien organisé, ce que je me suis efforcé de vous montrer depuis le début de ce cours.

Avec PHPUnit chaque test est effectué dans une application spécifique, il n’est donc pas possible de les rendre dépendants les uns des autres.

En général on utilise Mockery, un composant qui permet de simuler le comportement d’une classe. Il est déjà prévu dans l’installation de Laravel en mode développement :

"require-dev": {
    ...,
    "mockery/mockery": "^1.6",
    ...
    "phpunit/phpunit": "^10.5"
},

Le fait de prévoir ce composant uniquement pour le développement simplifie ensuite la mise en œuvre pour le déploiement. Normalement vous devriez trouver ce composant dans vos dossiers :

Simuler une classe

Nous allons voir maintenant comment l’utiliser mais pour cela on va mettre en place le code à tester. Ce ne sera pas trop réaliste mais c’est juste pour comprendre le mécanisme de fonctionnement de Mockery. Remplacez le code du contrôleur WelcomeController par celui-ci :

<?php
 
namespace App\Http\Controllers;
 
use App\Services\Livre;
use Illuminate\Routing\Controllers\HasMiddleware;
 
class WelcomeController extends Controller
{
    public static function middleware(): array
    {
        return [
            'guest',
        ];
    }
 
    public function index(Livre $livre)
    {
        $titre = $livre->getTitle();

        return view('welcome', compact('titre'));
    }
}
J’ai prévu l’injection d’une classe dans la méthode index. Voilà la classe en question :
<?php

namespace App\Services;

class Livre 
{
    public function getTitle() {
        return 'Titre';
    }
}

Bon d'accord ce n'est pas très joli mais c'est juste pour la démonstration...

La difficulté ici réside dans la présence de l’injection d’une classe. Comme on veut isoler les tests, l’idéal serait de pouvoir simuler cette classe. C’est justement ce que permet de faire Mockery.

Voici la classe de test que nous allons utiliser :

<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Services\Livre;

class WelcomeControllerTest extends TestCase
{
    public function testIndex(): void
    {
        // Création Mock
        $this->mock(Livre::class, function ($mock) {
            $mock->shouldReceive('getTitle')->andReturn('Titre');
        });

        // Action
        $response = $this->get('welcome');

        // Assertions
        $response->assertSuccessful();
        $response->assertViewHas('titre', 'Titre');
    }
}
Et voici le code à ajouter dans la vue pour faire réaliste :
{{ $titre }}
Si je lance le test ça se passe bien.

Voyons de plus près ce code... On crée un objet Mock en lui demandant de simuler la classe Livre :

$this->mock(Livre::class, function ($mock) {

Ensuite on définit le comportement que l’on désire pour cet objet :

->shouldReceive('getTitle')->andReturn('Titre');
On lui dit qu’il reçoit (shouldReceive) l’appel de la méthode getTitle et doit retourner Titre.

Ensuite on fait l’action, ici la requête :

$response = $this->get('welcome');

Pour finir on prévoit deux assertions, une pour vérifier qu’on a une réponse correcte et la seconde pour vérifier qu’on a bien le titre dans la vue :

$response->assertSuccessful();
$response->assertViewHas('titre', 'Titre');

Vous connaissez maintenant le principe de l’utilisation de Mockery. Il existe de vastes possibilités avec ce composant.

Il n’y a pas vraiment de règle quant à la constitution des tests, quant à ce qu’il faut tester ou pas. L’important est de comprendre comment les faire et de juger ce qui est utile ou pas selon les circonstances. Une façon efficace d’apprendre à réaliser des tests tout en comprenant mieux Laravel est de regarder comment ses tests ont été conçus.

En résumé

  • Laravel permet d'effectuer des tests unitaires.
  • En plus des méthodes de Pest ou PHPUnit on dispose d’helpers pour intégrer les tests dans une application réalisée avec Laravel.
  • Le composant Mockery permet de simuler le comportement d’une classe et donc de bien isoler les tests.
     


Par bestmomo

Aucun commentaire