• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 13/11/2023

Testez votre site web

Bien qu'apprendre PHPUnit ne soit pas un objectif de ce cours, le framework Symfony est livré avec différents outils pour vous aider à écrire tout type de test. Parmi ceux-là, nous parlerons du Bridge PHPUnit, du composant  BrowserKit et de Symfony Panther !

Validez vos objets avec Simple PHPUnit

Le projet Symfony ne recommande pas d'outil particulier pour tester vos objets à l'aide de tests unitaires ou fonctionnels, mais il fournit une bonne intégration à l'outil PHPUnit appelé "Bridge PHPUnit".

Le Bridge PHPUnit

Cette extension fournit des outils pour afficher des tests obsolètes, l'usage de fonctions dépréciées ou encore des fonctions utilitaires pour simuler des actions qui dépendent du temps ou du réseau. Il vient avec les fonctionnalités suivantes :

  • les tests seront tous exécutés avec une même locale ( c ) ;

  • les annotations Doctrine seront correctement autochargées ;

  • l'affichage de la liste des fonctionnalités dépréciées de l'application ;

  • des "bouchons" spécifiques pour le temps (ClockMock) et le réseau (DnsMock) ;

  • une version améliorée de PHPUnit qui ne dépend pas du composant Yaml ou de Prophecy.

Le bridge n'est pas forcément disponible dans tous les projets Symfony 4. Pour l'installer nous pouvons utiliser Composer :

➜ composer req --dev tests
Utilisation de Simple PHPUnit

Simple PHPUnit est la version améliorée de PHPUnit fournie par le bridge, qui installe une version de PHPUnit en supprimant la dépendance sur le composant Yaml et Prophecy.

Pour installer PHPUnit à partir de Simple PHPUnit, nous utiliserons la commande  install :

➜ ./vendor/bin/simple-phpunit install
symfony/yaml is not required in your composer.json and has not been removed
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies
Package operations: 22 installs, 0 updates, 0 removals
 - Installing doctrine/instantiator (1.1.0): Downloading (100%)
 - Installing phpunit/php-timer (2.0.0): Downloading (100%)
 - Installing sebastian/global-state (2.0.0): Downloading (100%)
 - Installing sebastian/recursion-context (3.0.0): Downloading (100%)
 - Installing sebastian/object-reflector (1.1.1): Downloading (100%)
 - Installing sebastian/object-enumerator (3.0.3): Downloading (100%)
 - Installing myclabs/deep-copy (1.8.1): Downloading (100%)
 - Installing phar-io/version (2.0.1): Downloading (100%)
 - Installing phar-io/manifest (1.0.3): Downloading (100%)
 - Installing theseer/tokenizer (1.1.0): Downloading (100%)
 - Installing sebastian/version (2.0.1): Downloading (100%)
 - Installing sebastian/environment (3.1.0): Downloading (100%)
 - Installing sebastian/code-unit-reverse-lookup (1.0.1): Downloading (100%)
 - Installing phpunit/php-token-stream (3.0.1): Downloading (100%)
 - Installing phpunit/php-text-template (1.2.1): Downloading (100%)
 - Installing phpunit/php-file-iterator (2.0.2): Downloading (100%)
 - Installing phpunit/php-code-coverage (6.1.4): Downloading (100%)
 - Installing sebastian/exporter (3.1.0): Downloading (100%)
 - Installing sebastian/diff (3.0.1): Downloading (100%)
 - Installing sebastian/comparator (3.0.2): Downloading (100%)
 - Installing sebastian/resource-operations (2.0.1): Downloading (100%)
 - Installing symfony/phpunit-bridge (7.4.99): Symlinking from /var/www/html/vendor/symfony/phpunit-bridge
Writing lock file
Generating optimized autoload files

Ensuite, Simple PHPUnit se comporte exactement comme PHPUnit :

➜ ./vendor/bin/simple-phpunit
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

Testing Project Test Suite
....................................... 39 / 39 (100%)

Time: 5.15 seconds, Memory: 52.25MB

OK (39 tests, 61 assertions)
Configuration de PHPUnit

Dans un projet Symfony 5, il faudra configurer PHPUnit correctement pour que les tests puissent être lancés. Voici une adaptation de la configuration d'un projet de démonstration :

<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.1/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="vendor/autoload.php"
>
    <php>
        <ini name="error_reporting" value="-1"/>
        <env name="KERNEL_CLASS" value="App\Kernel"/>
        <env name="SYMFONY_PHPUNIT_VERSION" value="7.5.20"/>
        <env name="SYMFONY_DEPRECATIONS_HELPER" value=""/>

        <!-- ###+ doctrine/doctrine-bundle ### -->
        <env name="DATABASE_URL" value="sqlite:///data/database_test.sqlite"/>
        <!-- ###- doctrine/doctrine-bundle ### -->

        <!-- ###+ symfony/swiftmailer-bundle ### -->
        <env name="MAILER_URL" value="null://localhost"/>
        <!-- ###- symfony/swiftmailer-bundle ### -->

        <!-- ###+ symfony/framework-bundle ### -->
        <env name="APP_ENV" value="test"/>
        <env name="APP_DEBUG" value="1"/>
        <env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e"/>
        <!-- ###- symfony/framework-bundle ### -->
    </php>

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

Certaines de ces lignes doivent vous étonner, même si vous connaissez bien PHPUnit :

  • KERNEL_CLASS  doit avoir comme valeur le chemin vers le kernel de votre application ;

  • SYMFONY_PHPUNIT_VERSION  n'est pas obligatoire et permet de fixer la version de PHPUnit installée par le bridge (important si vous comptez tester votre projet sur différentes versions de PHP) ;

  • SYMFONY_DEPRECATION_HELPER  n'est pas obligatoire et permet de configurer le gestionnaire de dépréciations ;

  • APP_ENV  permet d'exécuter la suite de tests en environnement de test ;

  • APP_DEBUG  active le mode de débogage du framework pendant l'exécution des tests ;

  • APP_SECRET peut également être configurée ; cette clé est utilisée par le composant Security pour générer des tokens, par exemple.

Gestion des fonctions dépréciées avec Simple PHPUnit

L'une des fonctionnalités les plus utiles du bridge est le gestionnaire de dépréciations. À mesure que les projets évoluent, nous finissons par rendre des fonctionnalités, et donc du code, obsolètes. Parfois, pour des questions de compatibilité, nous sommes amenés à devoir conserver ce code dans nos applications, même si nous ne l'utilisons plus directement.

Pour cela, il faut "lancer" une erreur de type E_DEPRECATED dans le corps de nos fonctions dépréciées :

<?php

namespace App\Tools;

class Exemple
{
    public function helloWorld()
    {
        @trigger_error('Cette fonction est dépréciée, utilisez la fonction hello($name) à la place.', E_USER_DEPRECATED);
        
        return 'Hello World !';
    }

    public function hello($name = 'World')
    {
        return 'Hello '. ucfirst($name);
    }
}

Grâce au gestionnaire de dépréciations, on obtient un rapport supplémentaire dans PHPUnit :

➜ ./vendor/bin/simple-phpunit
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

Testing Project Test Suite
....................................... 39 / 39 (100%)

Time: 4.72 seconds, Memory: 42.00MB

OK (39 tests, 61 assertions)

Remaining deprecation notices (3)

 3x: Cette fonction est dépréciée, utilisez la fonction hello($name) à la place.
 1x in ValidatorTest::testValidatePassword from App\Tests\Utils
 1x in ValidatorTest::testValidatePasswordEmpty from App\Tests\Utils
 1x in ValidatorTest::testValidatePasswordInvalid from App\Tests\Utils

L'écriture des tests unitaires dans Symfony se fait exactement de la même façon que dans n'importe quel projet PHP. Les assertions à votre disposition dépendront du framework de test que vous aurez choisi.

Validez l’accès à vos pages

Les tests fonctionnels vérifient l'intégration de différentes couches de l'application. Ils ne sont pas très différents des tests unitaires lorsqu'ils sont réalisés avec PHPUnit, mais ils suivent un cycle de vie spécifique :

  • effectuer une requête auprès de l'application ;

  • parcourir le DOM (le code HTML de la page) ;

  • contrôler la réponse du serveur.

Pour cela, Symfony dispose de deux composants qui permettent de faciliter le développement de tests fonctionnels : les composants BrowserKit et CssSelector. Pour les installer, il faudra utiliser Composer :

composer require --dev symfony/browser-kit symfony/css-selector

Une fois ces deux composants installés, nous pouvons utiliser la classe WebTestCase du framework Symfony. Cette classe étend la classe TestCase de PHPUnit et donne accès à un client de test qui permet d'exécuter des requêtes HTTP :

<?php
// tests/Controller/PostControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testShowPost()
    {
        $client = static::createClient();

        $client->request('GET', '/post/hello-world');

        self::assertSame(200, $client->getResponse()->getStatusCode());
    }
}

Parcourez le DOM avec le client de Symfony

Dans l'exemple précédent, une requête HTTP GET est effectuée sur l'application.

Le résultat de l'appel de cette fonction est un objet Crawler. Cet objet est particulièrement intéressant pour faire du test fonctionnel, car il va nous permettre de parcourir le DOM de la page reçue.

Améliorons l'exemple précédent pour effectuer un test sur le contenu de la réponse du serveur :

<?php
// tests/Controller/PostControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testShowPost()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/post/hello-world');
        
        self::assertSame(1, $crawler->filter('html:contains("Hello World")')->count());
    }
}

À l'aide de la fonction filter($selector) de l'objet Crawler, et parce que nous avons installé le composant CssSelector, nous pouvons utiliser des expressions CSS pour rechercher un contenu spécifique dans la page HTML reçue.

En complément des possibilités de requête du DOM, il est possible de simuler certaines actions utilisateurs comme le clic sur un lien :

<?php

$client->request('GET', '/');
$crawler = $client->clickLink('Conditions générales (CGU)');

// exemple de lien

<a href="https://some-link.com">Conditions générales (CGU)</a>

Cette action fonctionnera s'il y a un lien avec le nom "Conditions générales (CGU)" ou encore une image dont l'attribut "alt" serait égal à "Conditions générales (CGU)". Dans ce cas, le client va accéder à la nouvelle page en suivant le lien.

Pour simuler la soumission d'un formulaire, nous pouvons utiliser la fonction submitForm() :

<?php

$crawler = $client->submitForm('Créer le compte', [
    'username' => 'Léa',
    'password' => 'secr3t4ssw0rd',
]);

// exemple de formulaire

<form>
    <input type="text" name="username" />
    <input type="password" name="password" />
    
    <input type="submit" value="Créer le compte" />
</form>

Cette action fonctionnera s'il existe un bouton de soumission qui a pour valeur "Créer le compte" et deux entrées de formulaires avec les attributs "name", username et password.

Le bon réflexe : les tests de survie

Les tests de survie sont également appelés tests de fumée ("Smoke testing"). Ce sont des tests unitaires ou fonctionnels qui contrôlent les comportements critiques de l'application : si ces tests échouent, il est inutile d'écrire des tests plus approfondis, de faire évoluer ou de déployer l'application en production.

Ils doivent être rapides à écrire, rapides à exécuter et remonter des informations utiles à la résolution d'un bug si l'application ne fonctionne pas correctement.

Si vous souhaitez apprendre à utiliser Symfony, c'est probablement pour développer des sites web. Voici donc un exemple d'une suite de tests de fumée :

<?php
// tests/Controller/SmokeTest.php
namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class SmokeTest extends WebTestCase
{
    /**
     * @dataProvider provideUrls
     */
    public function testPageIsSuccessful($pageName, $url)
    {
        $client = self::createClient();
        $client->catchExceptions(false);
        $client->request('GET', $url);
        $response = $client->getResponse();
    
        self::assertTrue(
            $response->isSuccessful(),
            sprintf(
                'La page "%s" devrait être accessible, mais le code HTTP est "%s".',
                $pageName,
                $response->getStatusCode()
            )
        );
    }
    
    public function provideUrls()
    {
        return [
            'accueil' => ['Accueil', '/'],
            'produit' => ['Page produit', '/product/1'],
            'contact' => ['Formulaire de contact', '/contact'],
            'achat' => ['Process d\'achat', '/order'],
            'recherche' => ['Recherche Produit', '/search']
            // ...
        ];
    }
}

Si cette suite de tests passe, elle valide que chacune des pages "clés" du site web est accessible sans exception. Cela ne prouve pas que le contenu est bon, mais en tout cas, nos utilisateurs ne verront pas de page blanche ni de message d'erreur terrible en parcourant le site : rassurant !

À quoi sert la fonction catchExceptions() ?

C'est pour changer un comportement un peu embêtant du framework. Nous souhaitons évidemment lancer nos tests avec un maximum d'informations en cas d'erreur. Donc le mode de débogage est activé, ainsi que le Web Profiler.

Si une exception est levée lors de l'exécution du test, que va-t-il se passer ?

L'exception va être "attrapée" par le gestionnaire d'erreurs pour afficher une belle page avec toutes les informations pour corriger. Quand nous sommes en train de développer notre application, ce comportement est pratique, mais il ne l'est pas lorsque nous lançons nos tests en utilisant l'invite de commande.

Voici le résultat sans la désactivation de la récupération des exceptions :

➜ ./vendor/bin/simple-phpunit --filter="SmokeTest"
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

Testing Project Test Suite
....F 5 / 5 (100%)

Time: 1.72 seconds, Memory: 42.00MB

There was 1 failure:

1) Tests\SmokeTest::testPageIsSuccessful with data set "Recherche Produit" ('Recherche Produit')
La page "recherche" devrait être accessible, mais le code HTTP est "500"
Failed asserting that false is true.

Et maintenant avec la désactivation :

➜ ./vendor/bin/simple-phpunit --filter="SmokeTest"
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

Testing Project Test Suite
....F 5 / 5 (100%)

Time: 1.98 seconds, Memory: 42.00MB

There was 1 failure:

1) Tests\SmokeTest::testPageIsSuccessful with data set "Recherche Produit" ('Recherche Produit', '/search')
Exception: BOOM! Exception volontaire!

/home/dev/Projects/mon-projet/src/Controller/SearchController.php:59
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:151
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:68
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php:200
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Client.php:68
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Client.php:131
/home/dev/Projects/mon-projet/vendor/symfony/symfony/src/Symfony/Component/BrowserKit/Client.php:312
/home/dev/Projects/mon-projet/tests/SmokeTest.php:40

C'est quand même plus utile d'avoir l'exception avec le fichier et la ligne concernés, ainsi que la "trace" d'exécution, n'est-ce pas ? ;)

Validez l’interaction avec vos utilisateurs

Introduction à Symfony Panther

Symfony Panther est un outil de test navigateur et une librairie capable de retrouver de l'information sur le web (ce que l'on appelle le "web scrapping"). Nous l'avons vu, le client fourni par la classe WebTestCase n'utilise pas un vrai navigateur. À l'aide de PHP, le client simule des requêtes HTTP :

  • Il crée des requêtes à l'aide du composant HttpFoundation.

  • Ces requêtes sont soumises directement au kernel sans même effectuer une requête HTTP.

  • Et les assertions sont effectuées sur l'objet Response obtenu.

Mais que va-t-il se passer si le problème vient, non pas de l'application PHP/Symfony, mais du côté navigateur ?

Que ce soit un problème CSS (un bouton est caché et inaccessible), un problème JavaScript (le formulaire n'est pas affiché) ou même une erreur de validation navigateur (comme une validation HTML5 côté navigateur que nous aurions oublié de désactiver), Symfony Panther va nous permettre d'exécuter des scénarios utilisateurs dans de vrais navigateurs.

Votre premier test avec Panther

Va-t-il falloir réécrire nos tests précédemment créés avec BrowserKit et WebTestCase?

Pas forcément ! Car Symfony Panther implémente les mêmes API que les composants BrowserKit et DomCrawler utilisés dans notre test précédent.

Symfony Panther utilise la librairie WebDriver et celle-ci vient avec quelques contraintes :

  • vous ne pourrez pas récupérer le code HTTP de la réponse, donc impossible d'utiliser Panther pour faire des tests de fumée ;

  • vous ne récupérerez pas de réponse qui vient du composant HttpFoundation, il faut utiliser le Crawler pour parcourir le DOM.

Vous ne créerez donc pas tout à fait les mêmes tests avec Symfony Panther qu'avec BrowserKit.

Créons notre premier test pour utiliser Symfony Panther plutôt que BrowserKit pour lancer notre suite de tests :

<?php
// tests/Controller/SmokeTest.php
namespace App\Tests;

// use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Panther\PantherTestCase;

class SmokeTest extends PantherTestCase
{
    /**
     * @dataProvider provideUrls
     */
    public function testPageIsSuccessful($pageName, $url)
    {
        // $client = self::createClient(); fonctionne toujours, au besoin
        $client = static::createPantherClient();
        $crawler = $client->request('GET', $url);
    
        self::assertCount(
            1,
            $crawler->filter('title')->count(),
            sprintf(
                'La page "%s" devrait être accessible.',
                $pageName
            )
        );
    }
    
    // ...
}
➜ ./vendor/bin/simple-phpunit --filter="SmokeTest"
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

Testing Project Test Suite
..... 5 / 5 (100%)

Time: 2.42 seconds, Memory: 52.20MB

OK (5 tests, 5 assertions)

Manipulez un formulaire dans vos tests

Vous vous rappelez le chapitre sur les formulaires ? Nous disions dans ce chapitre que, si pour une raison quelconque le formulaire n'était pas accessible, parce qu'il était caché à l'utilisateur ou qu'une prévalidation écrite en JavaScript empêchait le formulaire d'être soumis, ce problème ne serait pas trouvé par les tests effectués avec BrowserKit.

Améliorons le test de création de comptes utilisateurs avec Symfony Panther :

<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class UserActionsTest extends PantherTestCase
{
    /**
     * Un utilisateur peut s'inscrire sur le Blog
     */
    public function testRegistration()
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/inscription');

        # Ce formulaire est généré en Javascript
        $client->waitFor('#inscription-form');
        
        # Soumission du formulaire
        $client->submitForm('Créer le compte', [
            'username' => 'Léa',
            'password' => 's3cr3tP4ssw0rd',
        ]);
       
        # Redirection de l'utilisateur nouvellement inscrit vers l'accueil
        $this->assertSame(self::$baseUri.'/', $client->getCurrentURL());
        
        # Notification de succès en Javascript
        $client->waitFor('#success-message');
        $this->assertSame('Bienvenue sur le blog Zozor', $crawler->filter('#success-message ol li:first-child')->text());
        
        # L'utilisateur est bien authentifié
        $this->assertSame('Léa', $crawler->filter('#user-profile span:first-child')->text());
    }
}

Ce code présente de nombreux avantages :

  • si le formulaire est caché en CSS, le test échouera ;

  • s'il y a une erreur JavaScript à l'affichage ou à la soumission du formulaire, le test échouera ;

  • en utilisant l'option  PANTHER_NO_HEADLESS=1 , vous pourrez vérifier ce qu'il se passe dans le navigateur. C'est très utile pour comprendre pourquoi un test échoue, alors que vous ne reproduisez pas le bug manuellement. ;)

En résumé

Tester une application Symfony ne demande pas d'apprendre à utiliser un nouvel outil si vous êtes à l'aise avec PHPUnit ou tout autre framework de test.

Si vous utilisez PHPUnit pour vos tests unitaires et fonctionnels, il est conseillé d'installer le bridge qui apporte quelques fonctionnalités utiles, dont le gestionnaire de dépréciations.

Vos tests fonctionnels seront faciles à écrire si vous utilisez le client de test et la classe WebTestCase. La limitation est que ces tests ne fonctionneront que pour des applications dont le moteur de gabarit est développé en PHP (comme Twig).

Pour les applications modernes avec un client JavaScript lourd (de type API REST + React ou Vue ou Angular), il faudra utiliser un outil capable d'utiliser un vrai navigateur.

Le client navigateur Panther est capable d'utiliser le moteur "Chrome", et le JavaScript et le CSS seront exécutés. Nous pourrons donc tester nos applications en simulant des comportements utilisateurs en conditions réelles.

Maintenant que notre application Symfony est bien testée et de qualité, il est temps de la mettre en ligne, n'est-ce pas ? C'est justement l'objet du prochain chapitre, alors à tout de suite ! :D

Exemple de certificat de réussite
Exemple de certificat de réussite