Development

Documentation/fr_FR/book/1.0/trunk/15-Unit-and-Functional-Testing

You must first sign up to be able to contribute.

Version 28 (modified by Pascal.Borreli, 10 years ago)
fixed link

Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

Chapitre 15 - Tests unitaires et tests fonctionnels

L'automatisation des tests est l'une des avancées majeures en développement depuis la programmation objet. Ceci prend toute sa dimension lors de la conception d'une application web et est gage de qualité même en présence de nombreuses releases. Symfony propose tout un panel d'outils qui facilite l'automatisation des tests, ce chapitre va nous les présenter.

Automatiser les Tests

Tout développeur ayant un tant soit peu d'expérience connait les implications en temps d'investissement pour mener à bien ses tests. Ecrire les tests, les exécuter puis en analyser les résultats est un travail pouvant s'avérer laborieux. En outre, les applications web sont en perpétuelle évolution ce qui conduit bien souvent à des remaniements de code donc à des risques de voir apparaître de nouvelles erreurs.

C'est pourquoi il est fortement conseillé, voire obligatoire, d'utiliser l'automatisation des tests afin d'avoir des conditions de développement optimales. Seul toute une batterie de tests peut garantir qu'une application aura bien le comportement auquel on s'attend. Même si on vient à remanier le code, les tests automatisés préviennent d'une régression accidentelle de l'application. En plus, le développeur est contraint de se plier à des règles d'écriture rigoureuses et standardisées capables d'êtres interprétées par des framework de tests.

D'autre part, la génération automatique de tests peut parfois remplacer la documentation en illustrant de manière claire ce qu'une application est censée faire. Une série de tests montrant les résultats attendus peut contribuer à la compréhension de la finalité d'une méthode.

Symfony applique ce principe à lui-même et tout son code est ainsi validé par l'utilisation de l'automatisation des tests. Les tests unitaires et fonctionnels ne sont pas fournis en standard mais peuvent êtres récupérés sur le dépot SVN ou en ligne à l'adresse http://www.symfony-project.com/trac/browser/trunk/test

Tests unitaires et tests fonctionnels

Les tests unitaires confirment qu'une portion de code fournit bien une sortie correcte en fonction de paramètres d'entrées donnés. Ils valident le fonctionnement de méthodes ou de fonctions pour les cas particuliers. Les tests unitaires ne traitent qu'un cas à la fois ce qui conduit à les répéter dans le cas ou une méthode réagit différement en fonction de certaines situations.

Les tests fonctionnels valident un dispositif complet et non pas une simple conversion d'entrées-sorties. Par exemple, un système de cache ne pourra être validé que par un test fonctionnel dans la mesure ou il nécessite plusieurs étapes : la première fois qu'une page est demandée, elle est envoyée au navigateur tandis que la deuxième fois, elle est prise dans le cache. Donc, les tests fonctionnels reposent sur des scénarios pour valider un processus. Symfony exige d'écrire des tests fonctionnels pour chaque action.

Pour des interactions complexes, ces tests peuvent faire défaut comme par exemple l'utilisation d'AJAX qui nécessite un navigateur avec javascript. Ceci implique l'utilisation d'un outil spécifique non fourni avec le framework. De même, seul un humain est capable de valider des effets visuels.

Si vous voulez une approche complète des tests automatisés, vous aurez probablement besoin d'une combinaison de ces trois méthodes. De toutes les façons, ayez comme directive de vous imposer des tests simples et lisibles.

NOTE Les tests automatisés travaillent par comparaison entre résultats et résultats attendus. Autrement dit, ils évaluent les assertions (expressions du genre $a == 2). La valeur d'une assertion est du type true ou false ( vrai ou faux ), ce qui détermine si le test passe ou échoue. On parle du mot "assertion" lorsque l'on utilise un contexte de tests automatisés.

Développement piloté par les tests ( Test-Driven Development : TDD)

Dans une approche méthodologique basé sur TDD, les tests sont écrits avant le code. Ceci va vous aider à vous concentrer sur ce que la fonction doit accomplir avant d'écrire son code. C'est une bonne méthode, à l'instar d'autres, telles que le développement agile ( Extreme Programming : XP ), pourtant elles aussi recommandées. Mais ce qui abondera en son sens est le fait indéniable que si vous n'écrivez pas les tests en premiers, vous ne les écrirez jamais.

Un exemple, imaginons que vous vouliez développez une fonction permettant de nettoyer une chaîne de caratères. Cette fonction enlève les espaces de début et de fin, remplace les caractères non alphabétique par le caractère souligné ( _ ) et convertit les majuscules en miniscules. Dans le cas d'un développement TDD, vous devrez réfléchir à toutes les situations possibles et fournir des exemples d'entrées avec leurs sorties respectives, comme illustré dans le tableau 15-1.

Table 15-1 - Liste de tests pour une fonction permettant de nettoyer une chaîne de caractères

Entrée | Sortie Attendue --------------------- | --------------------- " foo " | "foo" "foo bar" | "foo_bar" "-)foo:..=bar?" | "__foo____bar_" "FooBar" | "foobar" "Don't foo-bar me!" | "don_t_foo_bar_me_"

Vous pouvez, dès lors, écrire le test, l'exécuter et vérifier si celui-ci échoue. Dans l'affirmative, il faut alors modifier le code puis rejouer le test tant que celui-ci échoue. Il faut ensuite passer au test suivant et procéder de manière identique. Une fois tous les tests passés avec succès, la fonction est alors déclarée apte.

An application built with a test-driven methodology ends up with roughly as much test code as actual code. De plus, si vous ne voulez pas perdre du temps dans l'écriture de vos tests, veillez à ce que ceux-ci soient le plus simple possible.

NOTE Remanier le code d'une méthode risque de créer de nouveaux bogues qui n'apparaissaient pas avant. c'est donc pour cela qu'il est de bon ton de rejouer tous les tests avant le déploiement d'une nouvelle version de production d'une application. C'est ce que l'on appelle un test de régression.

Lime, le framework de test

Le monde PHP fournit moult framework de tests dont les plus connus sont PhPUnit et SimpleTest. Symfony possède le sien et il s'appelle Lime. Celui-ci est basé sur la librairie Perl Test::More et est compatible TAP, ce qui signifie que les résultats des tests seront affichés tels que définis dans ce protocole pour en faciliter leur lecture.

Lime fournit le support d'unité de test. Il est plus léger que certains autres frameworks et possède les avantages suivants :

* Il lance les tests dans une installation vierge afin d'éviter les effets de bord. Tous les framework de test ne garantissent pas un environnement propre pour chaque test.
* Lime propose des tests très faciles à lire ainsi que les comptes rendus en découlant. Sur les systèmes compatibles, Lime propose une coloration syntaxique permettant une analyse plus fine.
* Symfony lui même utilise Lime pour faire ses tests de régression donc on peut trouver beaucoup d'exemples de tests unitaires et de tests fonctionnels dans le code source.
* Le noyau de Lime est validé par des tests unitaires.
* Lime est écrit en PHP, très véloce et bien codé, le tout en un seul fichier unique et sans dépendance : lime.php

Les différents tests qui suivent utilisent la syntaxe de Lime. Ils peuvent être lancés dès la fin de l'installation de Symfony

NOTE Les tests unitaires et fonctionnels sont supposés ne pas être lancés sur un serveur en production. Ce sont des outils de développement et à ce titre, doivent être utilisés sur une machine de développement, pas sur le serveur.

Tests unitaires

Les tests unitaires de Symfony ne sont en fait que de simples fichiers PHP dont le nom se termine par Test.php et sont situés dans le répertoire test/unit/ de votre application. Ces fichiers ont une syntaxe simple et lisible.

A quoi ressemble un fichier de test unitaire?

Le listing 15-1 montre une batterie de tests typiques pour la fonction strtolower(). On commence par créer une instance d'un objet lime_test ( vous n'avez pas à vous préoccuper des paramètres maintenant ). Chaque test unitaire est ensuite appelé via une méthode de l'objet lime_test. Le dernier paramètre de cette méthode est une chaîne de caractères optionnelle servant à l'affichage de sortie.

Listing 15-1 - Exemple de fichier de test unitaire test/unit/strtolowerTest.php

[php] <?php

include(dirname(FILE).'/../bootstrap/unit.php'); require_once(dirname(FILE).'/../../lib/strtolower.php');

$t = new lime_test(7, new lime_output_color());

// strtolower() $t->diag('strtolower()'); $t->isa_ok(strtolower('Foo'), 'string', 'strtolower() renvoi une chaîne'); $t->is(strtolower('FOO'), 'foo', 'strtolower() transforme les caractères en minuscule'); $t->is(strtolower('foo'), 'foo', 'strtolower() les caractères minuscules restent inchangés'); $t->is(strtolower('12#?@~'), '12#?@~', 'strtolower() les caractères non-alphabétique restent inchangés'); $t->is(strtolower('FOO BAR'), 'foo bar', 'strtolower() leaves blanks alone'); $t->is(strtolower('FoO bAr'), 'foo bar', 'strtolower() deals with mixed case input'); $t->is(strtolower(''), 'foo', 'strtolower() transforme une chaîne vide en foo');

Lancement du test unitaire en ligne de commande à l'aide de la directive test-unit . La sortie est suffisement explicite et vous montre quels tests passent ainsi que ceux qui échouent. Voir ci-dessous le résultat de la sortie.

Listing 15-2 - Lancement d'un test unitaire en ligne de commande

    > symfony test-unit strtolower

    1..7
    # strtolower()
    ok 1 - strtolower() renvoi une chaîne
    ok 2 - strtolower() transforme les caractères en minuscule
    ok 3 - strtolower() les caractères minuscules restent inchangés
    ok 4 - strtolower() les caractères non-alphabétique restent inchangés
    ok 5 - strtolower() leaves blanks alone
    ok 6 - strtolower() deals with mixed case input
    not ok 7 - strtolower() transforme une chaîne vide en foo
    #     Failed test (.\batch\test.php at line 21)
    #            got: ''
    #       expected: 'foo'
    # Looks like you failed 1 tests of 7.

TIP Les include en début de fichier sont optionnels, mais rendent le script autonome, ce qui permet son exécution en dehors de Symfony en invoquant simplement php test/unit/strtolowerTest.php.

Les différentes méthodes de tests unitaires

L'objet lime_test possède de nombreuse méthodes comme listées dans le tableau 15-2.

Tableau 15-2 - Méthodes de l'objet lime_test

Method | Description ------------------------------------------- | ------------------------------------------------------------- diag($msg) | Affiche un commentaire mais n'effectue pas les tests ok($test, $msg) | Réalise le test et continue si son résultat est vrai is($value1, $value2, $msg) | Compare les deux valeurs et continue si elles sont égales (==) isnt($value1, $value2, $msg) | Compare les deux valeurs et continue si elles ne sont pas égales like($string, $regexp, $msg) | Vérifie la conformité d'une chaîne avec une expression régulière unlike($string, $regexp, $msg) | Vérifie la non conformité d'une chaîne avec une expression régulière cmp_ok($value1, $operator, $value2, $msg) | Compare deux arguments en utilisant un opérateur spécifique isa_ok($variable, $type, $msg) | Vérifie le type d'un argument isa_ok($object, $class, $msg) | Vérifie la classe dun objet can_ok($object, $method, $msg) | Vérifie la présence d'une méthode pour un objet ou une classe is_deeply($array1, $array2, $msg) | Vérifie que deux tableaux ont les mêmes valeurs include_ok($file, $msg) | Valide le fait qu'un fichier existe et qu'il est bien inclu fail() | Fait échouer le test ( utile pour valider une exception ) pass() | Fait passer le test ( utile pour valider une exception ) skip($msg, $nb_tests) | Compte pour $nb_tests - utile pour les tests conditionnels todo() | Compte pour un test - utile pour les tests qui ne sont pas encore écrits

La syntaxe est tout à fait claire. Notez que la plupart des méthodes acceptent une chaîne de caractères comme dernier argument. Cette chaîne sera affichée en cas de succès du test. A l'heure actuelle, la meilleure des solutions pour apprendre à utiliser ces méthodes est de toutes les tester. Voir à ce propos le listing 15-3 qui les propose toutes.

Listing 15-3 - Méthodes de test de l'objet lime_test dans test/unit/exampleTest.php

[php] <?php

include(dirname(__FILE__).'/../bootstrap/unit.php');

// Stub objects and functions for test purposes
class myObject
{
  public function myMethod()
  {
  }
}

function throw_an_exception()
{
  throw new Exception('exception thrown');
}

// Initialize the test object
$t = new lime_test(16, new lime_output_color());

$t->diag('hello world');
$t->ok(1 == '1', 'l\'opérateur == ignore le type');
$t->is(1, '1', 'une chaîne est convertie en nombre pour comparaison');
$t->isnt(0, 1, 'zéro et un ne sont pas égaux');
$t->like('test01', '/test\d+/', 'test01 suit bien le modèle de numérotation');
$t->unlike('tests01', '/test\d+/', 'tests01 ne suit pas le modèle de numérotation');
$t->cmp_ok(1, '<', 2, 'un est inférieur à deux');
$t->cmp_ok(1, '!==', true, 'un et vrai ne sont pas identique');
$t->isa_ok('foobar', 'string', '\'foobar\' est une chaîne');
$t->isa_ok(new myObject(), 'myObject', 'new crée une instance de la classe');
$t->can_ok(new myObject(), 'myMethod', 'la classe unObjet doit possédé une méthode myMethod');
$array1 = array(1, 2, array(1 => 'foo', 'a' => '4'));
$t->is_deeply($array1, array(1, 2, array(1 => 'foo', 'a' => '4')),
    'le premier et deuxième tableau sont identique');
$t->include_ok('./fooBar.php', 'le fichier fooBar.php a bien été inclu');

try
{
  throw_an_exception();
  $t->fail('aucun code ne doit être exécuté après la levée d'une exception');
}
catch (Exception $e)
{
  $t->pass('exception interceptée avec succès');
}

if (!isset($foobar))
{
  $t->skip('passe un test afin de garder le compteur de tests valide', 1);
}
else
{
  $t->ok($foobar, 'foobar');
}

$t->todo('plus qu\'un test à faire');

Vous trouverez beaucoup d'autres exemples d'utilisation de ces méthodes dans les tests unitaires de symfony.

TIP Peut être vous demandez-vous pourquoi utiliser is() au lieu de ok(), et bien tout simplement parceque is() est beaucoup plus bavard que ok() et que vous aurez ainsi plus d'informations qu'une simple phrase vous expliquant que le test a échoué.

Paramètres de test

The initialization of the lime_test object takes as its first parameter the number of tests that should be executed. If the number of tests finally executed differs from this number, the lime output warns you about it. For instance, the test set of Listing 15-3 outputs as Listing 15-4. The initialization stipulated that 16 tests were to run, but only 15 actually took place, so the output indicates this.

Listing 15-4 - Le comptage des tests aide à la planification

    > symfony test-unit example

    1..16
    # hello world
    ok 1 - l'opérateur == ignore le type
    ok 2 - une chaîne est convertie en nombre pour comparaison
    ok 3 - zéro et un ne sont pas égaux
    ok 4 - test01 suit bien le modèle de numérotation
    ok 5 - tests01 ne suit pas le modèle de numérotation
    ok 6 - un est inférieur à deux
    ok 7 - one and true are not identical
    ok 8 - 'foobar' est une chaîne
    ok 9 - new crée une instance de la classe
    ok 10 - la classe unObjet doit possédé une méthode uneMethode
    ok 11 - le premier et deuxième tableau sont identique
    not ok 12 - le fichier fooBar.php a bien été inclu
    #     Failed test (.\test\unit\testTest.php at line 27)
    #       Tried to include './fooBar.php'
    ok 13 - exception interceptée avec succès
    ok 14 # SKIP passe un test afin de garder le compteur de tests valide
    ok 15 # TODO plus qu'un test à faire
    # Looks like you planned 16 tests but only ran 15.
    # Looks like you failed 1 tests of 16.

La méthode diag() n'entre pas dans le décompte des tests. On l'utilise pour ajouter des commentaires afin que les sorties de vos tests soient claires et organisées. A l'inverse, les méthodes todo() et skip() comptent pour un test. Une combinaison de pass()/fail() à l'intérieur d'un bloc try/catch compte comme un test simple.

Une bonne statégie de test passe obligatoirement par la planification de leur nombre. Vous trouverez ceci fort utile pour valider vos propre tests, spécialement lors de cas complexes avec des conditions ou des exceptions. Si un des tests échoue, vous le verrez imédiatement puisque le nombre final de tests ne correspondera pas avec celui donné lors de l'initialisation.

Le second paramètre du constructeur est un objet permettant d'étendre la classe lime_output pour la sortie. La plupart du temps, comme les tests sont lancés en ligne de commande, la sortie utilise un objet lime_output_color afin de tirer profit de la coloration syntaxique lorsque celle-ci est disponible.

La commande test-unit

La commande test-unit, qui lance les tests unitaires à partir de la ligne de commande, attend comme argument soit un ou plusieurs noms de fichier, soit un nom de fichier générique avec joker. Voir le listing 15-5 pour plus de détails.

Listing 15-5 - Exécution des tests unitaires

    // Structure du répertoire des tests
    test/
         unit/
              myFunctionTest.php
              mySecondFunctionTest.php
              foo/
                  barTest.php

    > symfony test-unit myFunction                   ## Lance myFunctionTest.php
    > symfony test-unit myFunction mySecondFunction  ## Lance les tests myFunctionTest.php et mySecondFunctionTest.php
    > symfony test-unit 'foo/*'                      ## Lance barTest.php
    > symfony test-unit '*'                          ## Lance tout les tests ( de façon recursive )

Stubs, Fixtures, et Autoloading

Dans les tests unitaires, la fonctionnalité d'autoloading est désactivée par défaut. Chaque classe que vous utilisez pour un test doit être explicitement définie dans le fichier soit par include, soit par required comme le montre le listing 15-6.

Listing 15-6 - Utilisation de classes dans les tests unitaires

    [php]
    <?php

    include(dirname(__FILE__).'/../bootstrap/unit.php');

    include(dirname(__FILE__).'/../../config/config.php');
    require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');

    $t = new lime_test(7, new lime_output_color());

    // isPathAbsolute()
    $t->diag('isPathAbsolute()');
    $t->is(sfToolkit::isPathAbsolute('/test'), true,
        'isPathAbsolute() renvoi true si le chemin est absolu');

    $t->is(sfToolkit::isPathAbsolute('\\test'), true,
        'isPathAbsolute() renvoi true si le chemin est absolu');
    $t->is(sfToolkit::isPathAbsolute('C:\\test'), true,
        'isPathAbsolute() renvoi true si le chemin est absolu');

    $t->is(sfToolkit::isPathAbsolute('d:/test'), true,
        'isPathAbsolute() renvoi true si le chemin est absolu');
    $t->is(sfToolkit::isPathAbsolute('test'), false,
        'isPathAbsolute() renvoi false si le chemin est relatif');

    $t->is(sfToolkit::isPathAbsolute('../test'), false,
        'isPathAbsolute() renvoi false si le chemin est relatif');
    $t->is(sfToolkit::isPathAbsolute('..\\test'), false,
        'isPathAbsolute() renvoi false si le chemin est relatif');

Dans les tests unitaires, vous devez instancier non seulement l'objet que vous voulez tester, mais également les objets dont il dépend. Seulement, les tests unitaires doivent rester unitaires, or, dépendre d'autre classes peut faire échouer un ou plusieurs tests si l'une d'elles à un problème. De plus, le fait de travailler avec les vrais objet peut être couteux en nombre de ligne de code ainsi qu'en temps d'exécution. Keep in mind that speed is crucial in unit testing because developers quickly tire of a slow process. Il faut bien garder à l'esprit que la vitesse est cruciale dans l'écriture des tests unitaires parce qu'un développeur est rarement patient et qu'un processus lent pourrait rapidement l'agacer.

Donc, à partir du moment où il vous faudra inclure beaucoup d'autres scripts pour un test, vous aurez besoin d'un outil d'auto-chargement très simple. Pour cela, la classe sfCore ( qui devra être inclue manuellement ) fournit la méthode initSimpleAutoload() qui attend comme paramètre un chemin absolu vers un répertoire. Toutes les classes situées à partir de ce chemin seront automatiquement inclues. Par exemple, si vous voulez que toutes vos classes placées dans le répertoire $sf_symfony_lib_dir/util/ soient automatiquement chargées, il vous faudra commencer votre script comme ceci :

[php]
require_once($sf_symfony_lib_dir.'/util/sfCore.class.php');
sfCore::initSimpleAutoload($sf_symfony_lib_dir.'/util');

TIP Un objet Propel génère toute une cascade de classes et par conséquent, dès que vous voulez en tester une, autoloading devient nécessaire. Notez que pour que Propel puisse fonctionner correctement, vous devez inclure les fichiers situés dans le répertoire vendor/propel/ ( donc l'appel à sfCore devient sfCore::initSimpleAutoload(array(SF_ROOT_ DIR.'/lib/model', $sf_symfony_lib_dir.'/vendor/propel'));) et ajouter le noyau Propel à la liste des chemins d'include ( par l'appel de set_include_path($sf_symfony_lib_dir.'/vendor'.PATH_SEPARATOR.SF_ROOT_DIR.PATH_SEPARATOR.get_include_path() ) ).

Un autre outil complémentaire à l'auto-chargement, est l'utilisation de stubs. Un stub est une implémentation alternative d'une classe ou les méthodes réelles sont remplacées pour fournir des informations plausibles. Un stub émule donc le fonctionnement d'une classe mais sans les inconvénients. Un bon exemple d'utilisation de stubs est l'implémentation d'une connexion à une base de données ou l'interface avec un web service. Le listing 15-7 montre un test unitaire sur une API de cartographie reliée à une classe WebService. Au lieu d'appeler la vraie méthode fetch() de la vraie classe , le test utilise un stub qui renverra des données valides.

Listing 15-7 - Utilisation des Stubs dans les tests unitaires

[php]
require_once(dirname(__FILE__).'/../../lib/WebService.class.php');
require_once(dirname(__FILE__).'/../../lib/MapAPI.class.php');

class testWebService extends WebService
{
  public static function fetch()
  {
    return file_get_contents(dirname(__FILE__).'/fixtures/data/fake_web_service.xml');
  }
}

$myMap = new MapAPI();

$t = new lime_test(1, new lime_output_color());

$t->is($myMap->getMapSize(testWebService::fetch(), 100));

Les données de test peuvent être plus complexes que de simples chaînes ou qu'un appel à une méthode. Les données complexes font souvent référence à des fixtures qui sont généralement placées dans un fichier séparé pour plus de clarté du code, spécialement si elles sont utilisées dans plusieurs tests unitaires. A ce propos, ne pas oublier que Symfony peut facilement transformer un fichier YAML en un tableau à l'aide de la méthode sfYAML::load(). Cela signifie qu'au lieu d'écrire un long tableau, vous pouvez écrire un fichier YAML à l'instar de celui du Listing 15-8.

Listing 15-8 - Utilisé un fichier de Fixture dans les tests unitaires

[php]
// In fixtures.yml:
-
  input:   '/test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   '\\test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'C:\\test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'd:/test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative
-
  input:   '../test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative
-
  input:   '..\\test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative

// In testTest.php
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
require_once($sf_symfony_lib_dir.'/util/sfYaml.class.php');

$testCases = sfYaml::load(dirname(__FILE__).'/fixtures.yml');

$t = new lime_test(count($testCases), new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
foreach ($testCases as $case)
{
  $t->is(sfToolkit::isPathAbsolute($case['input']), $case['output'],$case['comment']);
}

Tests fonctionnels

Les tests fonctionnels valident toute une partie de votre application. Ils simulent une session de navigation, émettent des requêtes et vérifient l'intégrité des réponses comme vous le feriez manuellement pour valider le fait qu'une action effectue bien ce qu'elle est censée faire. Dans un test fonctionnel, vous suivez un scénario correspondant à un cas d'utilisation.

A quoi ressemble un test fonctionnel ?

Vous pourriez réaliser vos tests fonctionnels avec un éditeur de texte et moulte assertion d'expressions régulières, mais ce serait une belle perte de temps. Symfony fournit un objet spécial appelé sfBrowser qui agit comme un navigateur qui serait connecté à votre application et ce, sans avoir besoin de serveur et surtout sans la couche de transport HTTP gourmande en temps. Il donne accès au noyau objets à chaque requête (la requête, la session, le contexte et les objets renvoyés). Symfony fournit également une extension à cet objet, appellée sfTestBrowser, spécialement dédiée aux tests fonctionnels et qui possède toutes les facilités de sfBrowser, plus d'autres particulièrement futées.

Un test fonctionnel débute généralement par l'instanciation d'un objet de test de navigation. Cet objet va faire une requête à une action puis vérifier que des éléments sont bien présents dans la réponse.

Par exemple, chaque fois que vous générez un squelette de module avec les tâches init-module ou propel-init-crud, symfony crée une fonction de test fonctionnel pour ce module. Ce test émet une requête pour l'action par défaut du module et vérifie le code de retour de la réponse, le module et l'action générés par le système de routage et la présence de certaines chaînes dans le contenu de la réponse. Pour un module foobar, le fichier généré sera foobarActionsTest.php comme on peut le voir dans le listing 15-9.

Listing 15-9 - Test fonctionnel par défaut pour un nouveau module dans tests/functional/frontend/foobarActionsTest.php

[php]
<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$browser = new sfTestBrowser();
$browser->initialize();

$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '!/This is a temporary page/')
;

TIP La méthode de simulation du navigateur renvoie un objet sfTestBrowser, vous pouvez donc enchaîner les appels aux méthodes pour plus de lisibilité de vos fichiers de tests. c'est ce que l'on appelle la fluidité de l'interface objet puisque rien ne peut stopper le flux de ces appels.

Un test fonctionnel peut contenir plusieurs requêtes ainsi que des assertions plus complexes; vous découvrirez bientôt toutes les possibilités offertes dans les sections à venir.

Pour lancer un test fonctionnel, utilisez la tâche test-functional en ligne de commande comme indiqué dans le listing 15-10. Cette tâche attend un nom d'application ainsi que le nom d'un test (en ne mettant pas le suffixe Test.php).

Listing 15-10 - Lancer un simple test fonctionnel à partir de la ligne de commande

> symfony test-functional frontend foobarActions

# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
not ok 4 - response selector body does not match regex /This is a temporary page/
# Looks like you failed 1 tests of 4.
1..4

Un test fonctionnel généré pour un nouveau module échouera par défaut. Ceci est du au fait que lors de la création d'un nouveau module, l'appel de l'index est redirigé vers une page de félicitations ( qui est inclue dans le module default de symfony ) qui contient la phrase "This is a temporary page." Tant que vous ne modifierez pas l'action liée à l'index, le test sur ce module échouera. Ceci garantit le fait de ne pouvoir passer avec succès le test d'un module inachevé.

NOTE Dans les test fonctionnels, l'auto-chargement est désactivé. En conséquence, vous devrez inclure les fichiers à la main.

Naviguation avec l'objet sfTestBrowser

Le navigateur de test est capable de générer des requêtes GET et POST. Dans les deux cas, on utilise une URI réelle en paramètre. Le listing 155-11 montre comment implémenter l'appel à l'objet sfTestBrowser pour simuler des requêtes.

Listing 15-11 - Simulation de requêtes gràce à l'objet sfTestBrowser

[php]
include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$b = new sfTestBrowser();
$b->initialize();

$b->get('/foobar/show/id/1');                   // GET request
$b->post('/foobar/show', array('id' => 1));     // POST request

// The get() and post() methods are shortcuts to the call() method
$b->call('/foobar/show/id/1', 'get');
$b->call('/foobar/show', 'post', array('id' => 1));

// The call() method can simulate requests with any method
$b->call('/foobar/show/id/1', 'head');
$b->call('/foobar/add/id/1', 'put');
$b->call('/foobar/delete/id/1', 'delete');

Une session de navigation typique ne contient pas uniquement des requêtes dues à des actions spécifiques, mais également des clicks sur des liens et/ou sur des boutons. Le listing 15-12 montre que l'objet sfTestBrowser en est également capable.

Listing 15-12 - Simulation de navigation gràce à lobjet sfTestBrowser

[php]
$b->get('/');                  // Request to the home page
$b->get('/foobar/show/id/1');
$b->back();                    // Back to one page in history
$b->forward();                 // Forward one page in history
$b->reload();                  // Reload current page
$b->click('go');               // Look for a 'go' link or button and click it

Le navigateur de test conserve un historique de la navigation, en conséquence, les méthodes back() et forward() fonctionne à l'instar d'un vrai navigateur.

TIP Le navigateur de test possède son propre mécanisme de gestion de sessions (sfTestStorage) et de cookies

Parmis les interactions devant être testées, celle mettant en oeuvre les formulaires arrive en premier. Pour simuler des entrées dans un formulaire puis sa soumission, vous avez trois choix. Vous pouvez aussi bien créer une requête POST avec les paramètres que vous souhaitez soumettre, passez par la méthode click() en lui fournissant comme paramètre un tableau contenant les entrées du formulaire ou alors complèter un à un les champs du formulaire puis cliquer sur le bouton submit. De toute façon, tout les résultats se retrouveront dans la requête POST. Le listing 15-13 montre un tel exemple.

Listing 15-13 - Simulation de saisie de formulaire avec l'objet sfTestBrowser

[php]
// Example template in modules/foobar/templates/editSuccess.php
<?php echo form_tag('foobar/update') ?>
  <?php echo input_hidden_tag('id', $sf_params->get('id')) ?>
  <?php echo input_tag('name', 'foo') ?>
  <?php echo submit_tag('go') ?>
  <?php echo textarea('text1', 'foo') ?>
  <?php echo textarea('text2', 'bar') ?>
</form>

// Exemple de test fonctionnel basé sur le formulaire précédent
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');

// Option 1: Requête POST 
$b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));

// Option 2: Clique sur le bouton submit avec un tableau de paramètres
$b->click('go', array('name' => 'dummy'));

// Option 3: Remplissage des champs un par un avec leur valeur associées puis clique sur le bouton submit
$b->setField('name', 'dummy')->
    click('go');

NOTE Les valeurs par défaut des champs du formulaire sont automatiquement complétés lors de l'utilisation des options 2 ou 3. La cible du formulaire n'a pas non plus à être spécifiée.

Lorsqu'une action finie par une redirection ( redirect() ), le navigateur de test n'en tient pas compte. En conséquence, vous devrez l'écrire explicitement avec la méthode followRedirect() comme le montre le listing 15-14.

Listing 15-14 - Le navigateur de test ne suit pas automatiquement les redirections

[php]
// Example action in modules/foobar/actions/actions.class.php
public function executeUpdate()
{
  ...
  $this->redirect('foobar/show?id='.$this->getRequestParameter('id'));
}

// Example functional test for this action
$b = new sfTestBrowser();   
$b->initialize();
$b->get('/foobar/edit?id=1')->
    click('go', array('name' => 'dummy'))->
    isRedirected()->   // Vérifie si la requête est redirigée
    followRedirect();    // On suit cette redirection de façon explicite

Il reste un dernière méthode à connaître puvant s'avérer utile à la navigation : restart(). Cette méthode ré-initialise l'historique du navigateur, la session et les cookies comme si vous re-démarriez votre navigateur.

Une fois la première requête effectuée, l'objet sfTestBrowser donne accès à la requête, son contexte ainsi qu'aux objets réponses. Cela signifie que vous pouvez vérifier un grand nombre de choses comme la totalité du contenu texte en passant par les entêtes, les paramètres de la requêtes et la configuration.

[php]
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

SIDEBAR L'objet sfBrowser

Toutes les méthodes présentées dans les listing 15-10 à 15-13 sont également disponibles en dehors du contexte de test via l'objet sfBrowser. Vous pouvez les appeller comme suit :

[php]
// Create a new browser
$b = new sfBrowser();
$b->initialize();
$b->get('/foobar/show/id/1')->
    setField('name', 'dummy')->
    click('go');
$content = $b()->getResponse()->getContent();
...

L'objet sfBrowser est un outil très pratique pour faire des scripts batch, par exemple, si vous désirez générer une mise en cache d'une liste de pages (voir le chapitre 18 pour exemple détaillé).