• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Itération 11 : préparation pour la production

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Le but de cette itération est d'intégrer à l'application plusieurs améliorations dans l'objectif d'une future mise en production.

Ajout de tests fonctionnels

Pourquoi tester ?

La problématique des tests est souvent considérée comme secondaire et négligée par les développeurs débutants. C'est une erreur : lorsqu'on livre une application et qu'elle est placée en production (offerte à ses utilisateurs), il est essentiel d'avoir un maximum de garanties sur son bon fonctionnement afin d'éviter au maximum de coûteuses mauvaises surprises.

Le test d'une application peut être manuel. Dans ce cas, une personne effectue sur l'application une suite d'opérations prévue à l'avance (navigation, connexion, envoi d'informations...) pour vérifier qu'elle possède bien le comportement attendu. C'est un processus coûteux en temps et sujets aux erreurs (oublis, négligences, etc.).

En complément de ces tests manuels, on a tout intérêt à intégrer à un projet logiciel des tests automatisés qui pourront être lancés aussi souvent que nécessaire. Ceci est d'autant plus vrai pour les méthodologies agiles basées sur un développement itératif et des livraisons fréquentes, ou bien lorsque l'on met en place une intégration continue.

Comment tester ?

On peut employer différentes stratégies pour automatiser le test d'une application. Parmi les types de test possibles, citons les tests unitaires qui testent individuellement chaque élément de l'application (un composant, une classe, etc) et les tests fonctionnels qui vérifient le fonctionnement global de l'application.

Quelle que soit la stratégie choisie, l'écriture de tests est une activité chronophage et parfois délicate. En suivant les recommandations des bonnes pratiques Symfony, nous allons nous contenter d'écrire des tests fonctionnels simples. Ces tests vérifieront uniquement que l'application répond sans erreur aux différentes routes possibles.

Composants nécessaires

Nous allons créer nos tests à l'aide de PHPUnit, l'outil le plus fréquemment employé pour écrire des tests en PHP. Pour récupérer PHPUnit ainsi que les composants Symfony nécessaires, il faut modifier le fichiercomposer.json. Éditez ce fichier pour qu'il ait le contenu suivant. 

{
    "require": {
        ...
    },
    "require-dev": {
        "phpunit/phpunit": "~4.8",
        "symfony/browser-kit": "~2.8|3.0.*",
        "symfony/css-selector": "~2.8|3.0.*"
    },
    "autoload": {
        ...
    }
}

 

On utilise icirequire-dev pour définir les dépendances nécessaires uniquement pendant le développement. Lors de la mise en production de l'application, on utilisera Composer avec l'option--no-dev pour ne pas installer ces dépendances.

Il ne reste plus qu'à récupérer ces composants via la commande habituelle :

composer update

Classe de test

Le framework Silex permet d'écrire des tests PHPUnit sous la forme de classes dérivées de WebTestCase.

Dans le répertoire racine de l'application, créez l'arborescence de répertoires tests/Tests. Dans ce dernier répertoire, ajoutez un fichier nomméAppTest.php ayant le contenu suivant.

<?php

namespace MicroCMS\Tests;

require_once __DIR__.'/../../vendor/autoload.php';

use Silex\WebTestCase;

class AppTest extends WebTestCase
{
    /** 
     * Basic, application-wide functional test inspired by Symfony best practices.
     * Simply checks that all application URLs load successfully.
     * During test execution, this method is called for each URL returned by the provideUrls method.
     *
     * @dataProvider provideUrls 
     */
    public function testPageIsSuccessful($url)
    {
        $client = $this->createClient();
        $client->request('GET', $url);

        $this->assertTrue($client->getResponse()->isSuccessful());
    }

    /**
     * {@inheritDoc}
     */
    public function createApplication()
    {
        $app = new \Silex\Application();

        require __DIR__.'/../../app/config/dev.php';
        require __DIR__.'/../../app/app.php';
        require __DIR__.'/../../app/routes.php';
        
        // Generate raw exceptions instead of HTML pages if errors occur
        unset($app['exception_handler']);
        // Simulate sessions for testing
        $app['session.test'] = true;
        // Enable anonymous access to admin zone
        $app['security.access_rules'] = array();

        return $app;
    }

    /**
     * Provides all valid application URLs.
     *
     * @return array The list of all valid application URLs.
     */
    public function provideUrls()
    {
        return array(
            array('/'),
            array('/article/1'),
            array('/login'),
            array('/admin'),
            array('/admin/article/add'),
            array('/admin/article/1/edit'),
            array('/admin/comment/1/edit'),
            array('/admin/user/add'),
            array('/admin/user/1/edit'),
            ); 
    }
}

 

Ce fichier définit une classeAppTest dérivée deWebTestCase. Sa méthodecreateApplication instancie, configure et renvoie notre application Silex. Sa méthodeprovideUrls définit toutes les URL à tester : elles correspondent aux routes de notre application accessibles via la commande HTTP GET. Les routes testées ici correspondent à la page d'accueil, l'affichage de l'article ayant l'identifiant 1, le formulaire de connexion et les différentes pages du back-office.

Enfin, la méthodetestPageIsSuccessful de la classeAppTest instancie un client et vérifie (méthode PHPUnitAssertTrue) que la réponse HTTP renvoyée pour chaque URL à tester indique un succès. Dans le cas contraire, le test échouera.

Configuration pour les tests

Afin de faciliter le lancement des tests, on ajoute à l'application un fichier de configuration PHPUnit nomméphpunit.xml.dist. Créez ce fichier dans le répertoire racine et ajoutez-lui le contenu suivant.

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

<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
    backupGlobals               = "false"
    backupStaticAttributes      = "false"
    colors                      = "true"
    convertErrorsToExceptions   = "true"
    convertNoticesToExceptions  = "true"
    convertWarningsToExceptions = "true"
    processIsolation            = "false"
    stopOnFailure               = "false"
    syntaxCheck                 = "false"
    bootstrap                   = "vendor/autoload.php">

    <testsuites>
        <testsuite name="MicroCMS">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

 

Grâce à ce fichier, PHPUnit utilisera toutes les classes de test se trouvant dans le répertoiretests ainsi que ses sous-répertoires. L'arborescence de notre projet est maintenant la suivante.

Arborescence des tests du projet

Modifiez ensuite le fichierapp/config/dev.php comme indiqué ci-dessous.

<?php

// Doctrine (db)
$app['db.options'] = array(
    'driver'   => 'pdo_mysql',
    'charset'  => 'utf8',
    'host'     => '127.0.0.1',  // Mandatory for PHPUnit testing
    'port'     => '3306',
    'dbname'   => 'microcms',
    'user'     => 'microcms_user',
    'password' => 'secret',
);

// enable the debug mode
$app['debug'] = true;

Plutôt que d'importer comme auparavant le contenu deapp/config/prod.php, on définit les paramètres de connexion à la base de données en utilisant127.0.0.1 plutôt quelocalhost. Cela permet d'éviter un problème de connexion à MySQL pendant l'exécution des tests.

Exécution des tests

Il est temps de vérifier le fonctionnement de l'application ! Dans une fenêtre de terminal, déplacez-vous dans le répertoire racineMicroCMS puis lancez la commande qui démarre l'exécution des tests. Celle-ci dépend de votre système d'exploitation.

Sous Mac OS et Linux, tapez la commande ci-dessous :

vendor/bin/phpunit

Sous Windows, vous devez taper :

vendor\bin\phpunit.bat

Vous devriez obtenir un résultat similaire au suivant.

Ce résultat indique que 9 tests sur 9 ont réussi. Cela signifie que notre application répond avec succès aux requêtes vers les URL définies plus haut dans la méthodeprovideUrls. Même si ces tests ne couvrent pas tous les cas de figure et ne vérifient pas l'intégralité du code source, ils permettront d'éviter les erreurs majeures. On parle en anglais de smoke testing.

Journalisation

La journalisation consiste à mémoriser, le plus souvent dans un ou plusieurs fichiers textes, les événements qui se produisent lors du fonctionnement d'une application.

La journalisation peut fournir une aide bienvenue lors des phases de débogage. Elle est de plus très facile à mettre en oeuvre grâce à l'intégration par Silex de la librairie Monolog

Comme d'habitude, nous commençons par déclarer les dépendances nécessaires dans le fichiercomposer.json.

{
    "require": {
        ...,
        "symfony/monolog-bridge": "~2.8|3.0.*"
    },
    ...
}

Ensuite, on récupère le composantsmonolog-bridge  avec Composer.

composer update

Il faut maintenant configurer notre application Silex pour qu'elle utilise ces composants. Le comportement souhaité dépend du contexte :

  • En phase de développement, l'application doit produire plus d'événements de journalisation (pour aider au débogage).

  • En phase de production, le nombre d'événements de journalisation doit être réduit pour limiter les ralentissements.

Pour obtenir de comportement, ajoutez les lignes suivantes à la fin du fichierapp/config/prod.php.

<?php

// ...

// define log level
$app['monolog.level'] = 'WARNING';

La journalisation des événements se fera avec le niveau WARNING. Seuls les avertissements et les erreurs seront journalisés.

Ajoutez ensuite les lignes suivantes à la fin du fichierapp/config/dev.php.

<?php

// ...

// define log level
$app['monolog.level'] = 'INFO';

On définit le niveau de journalisation à INFO, ce qui permet d'enregistrer les informations en plus des avertissements et des erreurs.

À présent, modifiez le fichierapp/app.php en ajoutant les lignes ci-dessous à la fin de l'enregistrement des fournisseurs de services.

<?php

// ...

// Register service providers
// ...
$app->register(new Silex\Provider\MonologServiceProvider(), array(
    'monolog.logfile' => __DIR__.'/../var/logs/microcms.log',
    'monolog.name' => 'MicroCMS',
    'monolog.level' => $app['monolog.level']
));

// ...

Monolog est configuré avec le niveau de journalisation défini plus haut. Les événements de journalisation seront enregistrés dans le fichiervar/logs/microcms.log. La barre d'outils Symfony n'est activée que lorsque l'application est configurée pour le débogage (uniquement lorsqu'on utiliseapp/config/dev.php).

Enfin, créez dans le répertoire racine le sous-répertoire var/logs .

À présent, le fichiervar/logs/microcms.log enregistre les principaux événements de l'application. En voici un exemple.

[2014-11-10 17:16:46] MicroCMS.INFO: Matched route "GET_" (parameters: "_controller": "{}", "_route": "GET_") [] []
[2014-11-10 17:16:46] MicroCMS.INFO: > GET / [] []
[2014-11-10 17:16:46] MicroCMS.INFO: < 200 [] []
[2014-11-10 17:16:46] MicroCMS.INFO: Matched route "_wdt" (parameters: "_controller": "web_profiler.controller. profiler:toolbarAction", "token": "c58b8c", "_route": "_wdt") [] []
[2014-11-10 17:16:46] MicroCMS.INFO: > GET /_profiler/wdt/c58b8c [] []
[2014-11-10 17:16:47] MicroCMS.INFO: < 200 [] []
[2014-11-10 17:16:56] MicroCMS.INFO: Matched route "_article_id" (parameters: "_controller": "{}", "id": "1", "_ route": "_article_id") [] []
[2014-11-10 17:16:56] MicroCMS.INFO: > GET /article/1 [] []
[2014-11-10 17:16:57] MicroCMS.INFO: < 200 [] []
[2014-11-10 17:16:57] MicroCMS.INFO: Matched route "_wdt" (parameters: "_controller": "web_profiler.controller. profiler:toolbarAction", "token": "c47a8b", "_route": "_wdt") [] []
[2014-11-10 17:16:57] MicroCMS.INFO: > GET /_profiler/wdt/c47a8b [] []
[2014-11-10 17:16:58] MicroCMS.INFO: < 200 [] []

Pour l'instant, seuls les composants Silex/Symfony ajoutent des événements dans ce fichier. Pour que notre application en crée aussi, il suffirait de rajouter aux endroits appropriés de notre code source des appels de la forme :

$app['monolog']->addInfo("Ceci est un évènement de test");

Gestion des erreurs

En cas d'apparition d'une erreur (ressource non trouvée, problème de connexion à la base, etc.), l'application actuelle affiche directement le message associé, ainsi que la pile des appels (stack trace) en configuration de débogage. Voici par exemple ce qui se produit lorsqu'on tente d'accéder à l'URL http://microcms/dummy, non gérée par l'application.

Ce comportement est acceptable et même pratique en phase de développement, mais pas en production. Il faudrait que l'affichage des erreurs soit homogène avec le reste des vues. Pour obtenir ce comportement, nous allons ajouter à l'application un gestionnaire d'erreurs personnalisé.

Modifiez le fichierapp/app.php pour y ajouter le code ci-dessous en fin de fichier.

<?php

// ...
use Symfony\Component\HttpFoundation\Request;

// ...

// Register error handler
$app->error(function (\Exception $e, Request $request, $code) use ($app) {
    switch ($code) {
        case 403:
            $message = 'Access denied.';
            break;
        case 404:
            $message = 'The requested resource could not be found.';
            break;
        default:
            $message = "Something went wrong.";
    }
    return $app['twig']->render('error.html.twig', array('message' => $message));
});

Ce gestionnaire d'erreurs construit un message en fonction du code de l'erreur, puis génère la vueerror.html.twig en lui passant ce message en paramètre.

Créez le fichierviews/error.html.twig avec le contenu ci-dessous.

{% extends "layout.html.twig" %}

{% block title %}Error!{% endblock %}

{% block content %}
<div class="row" id="errorPanel">
    <div class="col-xs-5">
        <img class="img-responsive pull-right" src="{{ asset('/images/404-ghost.png') }}" alt="Error ghost"/>
    </div>
    <div class="col-xs-6">
        <h1>Whoops...<br><small>{{ message }}</small></h1>
    </div>
</div>
{% endblock %}

 

L'image404-ghost.png fait partie du code source de la plate-forme de blogging Ghost. Vous pouvez la télécharger ici puis la copier dans le répertoireweb/images (à créer).

Modifiez ensuite le fichierweb/css/microcms.css pour y ajouter le contenu ci-dessous, qui permet de placer correctement le message d'erreur dans la vue.

#errorPanel {
    padding-top: 30px;
    padding-bottom: 10px;
}

À présent, déclenchons volontairement une erreur en accédant à l'URL http://microcms/dummy. Voici le résultat obtenu.‌

Le code source associé à cette itération est disponible sur une branche du dépôt GitHub

Mise en production

Lorsque l'application sera mise en production, il faudra utiliser le fichierapp/config/prod.php à la place deapp/config/dev.php dans le contrôleur frontalweb/index.php.

<?php

require_once __DIR__.'/../vendor/autoload.php';

$app = new Silex\Application();

require __DIR__.'/../app/config/prod.php'; // Config for production
require __DIR__.'/../app/app.php';
require __DIR__.'/../app/routes.php';

$app->run();

Sur le serveur de production, il faudra installer les dépendances avec l'option--no-dev.

composer install --no-dev

 Bilan

Au cours de cette itération, nous avons ajouté à l'application des tests automatisés qui, malgré leur simplicité, permettront d'augmenter la confiance dans son fonctionnement. Nous avons également facilité la mise au point de l'application grâce à la journalisation. Enfin, l'apparition d'une erreur ne dégrade plus l'affichage.

La prochaine itération va permettre d'interagir avec notre application par le biais d'une API.‌

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