Tests sous Symfony2

conseils Symfony2

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Encore très récemment je me limitais à écrire uniquement tests d'accès aux données. Ils vérifiaient si l'utilisateur X peut voir l'élément ne lui appartenant pas. Dans cette situation chaque modification d'une autre partie (par exemple traitement d'un nouveau champ du formulaire) exigeais le passage par interface pour pouvoir tester la nouveauté. C'est alors que je me suis intéressé aux tests automatisés.

L'article, après une introduction théorique, abordera l'élaboration des tests dans Symfony2 avec l'utilisation de PHPUnit. On verra à travers cette partie pratique comment mettre en place les tests automatisés, auxquels problèmes on peut être confronté et quels peuvent être les solutions.

Le test te dirigera
La notion des tests automatisés regroupera dans cet article les tests unitaires et fonctionnels, gérés par Symfony2 avec le support du PHPUnit. Les deux vérifications s'appliquent dans les situations différentes.

Un test unitaire sera utilisé dans la situation où un développeur voudra vérifier le fonctionnement d'un petit bout de code. Il peut s'agir par exemple du résultat d'une méthode. On valide alors si une fonction retourne ce qu'on attend.

Un test fonctionnel est employé dans les situations plus complexes. Imaginons qu'on veut voir si en soumettant un formulaire on réussira à insérer un nouvel élément dans la base et à envoyer un e-mail à l'administrateur du système. Sous Symfony2 les tests fonctionnels sont utilisés avec un workflow basé sur : l'envoie d'une requête, la réception et le traitement de la réponse. Ces étapes peuvent être dupliquées plusieurs fois, en fonction du scénario du test.

Symfony2 utilise pour ses tests le framework PHPUnit. Développé depuis 2005 par Sebastian Bergmann, ce système est devenu une référence dans le milieu des développeurs web. Il est rapide à mettre en place. Les tests sont écrits avec une facilité étonnante. En plus, il est sans cesse amélioré. Sensio Labs a donc misé sur lui en l'intégrant dans la deuxième version du Symfony.

Mise en place des tests sous Symfony2
La façon d'écrire les tests est très claire. Chaque bundle contient un répertoire Tests/. C'est dedans qu'on place nos tests. Attention, le contenu de ce dossier devrait reprendre la structure du bundle en question. Si par exemple on s'apprête à tester les contrôleurs, on devrait placer dans Tests/ un répertoire Controller qui contiendra les éléments passés au crible.

Dans notre exemple on examinera l'action d'enregistrement d'un nouvel utilisateur. Pour ce faire, on rajoutera un nouvel environnement à notre application. A part du "dev" et "prod", elle va désormais gérer le mode "test". Les fichiers placés dans /app/config, suivi de suffixe "_test", seront interprétés pour cet environnement. Vu qu'on va effectuer les opérations sur la base de données, on va créer une deuxième base. Notre contrôleur de test se présentera ainsi :

namespace Users\UserBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class UserControllerTest extends WebTestCase
{

/**
* Values to test.
* @access private
* @var array
*/
private $userData = array('Form' => array(
'login' => "newUser", 'pass1' => "newUser", 'pass2' => "newUser",
'email' => "newUser@test.com")
);

/**
* Tests register action.
* @return Displayed template.
*/
public function testRegisterUser()
{
// create client
$client = static::createClient(array(
'environment' => 'test',
));
$client->followRedirects(false);
// post the form data
$crawler = $client->request('POST', 'register', $this->userData);
// DEBUG MODE - you can see the response returned by the tested action
file_put_contents($_SERVER['DOCUMENT_ROOT'].'response_test.txt', $client->getResponse());
$this->assertContains('registered_successfully', $client->getResponse()->getContent());
}

}

Voici l'explication des objets utilisés par cette méthode de test :
- $client - il permet de gérer le déroulement des tests. Il rend possible la soumission des requêtes, la manipulation de la navigation (rechargement de la page, redirection vers une autre adresse) ainsi que l'accès à des container, requête, response, kernel.
- $crawler - une sorte de robot qui permet d'interpréter la réponse. Grâce à lui on peut, par exemple, vérifier si la réponse contient un fragment de texte, une balise HTML spécifique, ou un noeud avec une classe CSS déterminé.

Dans le code ci-dessus un $client qui ne suit pas les redirections (followRedirects(false)) est initialisé. Ensuite un $crawler est créé pour une requête POST vers l'url /register. Ensuite il transmet les données traités par la partie effectuant l'inscription d'un nouvel utilisateur. Vers la fin la fonction du PHPUnit, assertContains(), vérifie si la réponse contient le fragment 'registered_successfully'. Simple, n'est-ce pas ?

Troubleshooting
Paradoxalement, l'écriture des tests peut aussi provoquer des problèmes. Les développeurs sous Windows (dont je fais encore partie) peuvent rencontrer des difficultés liés à la lecture du fichier de configuration du PHP. En fait après l'installation du PHPUnit et avant le lancement du premier tests vous pouvez voir des messages vous disant de déterminer la zone du temps (date.timezone) ou d'activer les extensions SQLLite ("You need to enable either the SQLite or PDO_SQLite extension for the profiler to run properly"). Une des solutions est le transfert du fichier php.ini dans le répertoire Windows et la détermination d'un chemin vers le dossier contenant les extensions installées (directive extension_dir).

Après le lancement de premiers tests vous pouvez voir des exceptions liées à ob_end_clean(), comme par exemple "ErrorException: Notice: ob_end_clean(): failed to delete buffer. No buffer to delete in.". Le fix rapide qui m'a aidé se trouve sur le Github de Zenklys.

Un autre problème concernait les utilisateurs connectés. L'application testée utilise parfois les données appartenant à des internautes authentifiés (par exemple leurs identifiants, logins, adresses e-mail). Après avoir échoué à passer mon AuthenticationToken via $client, j'ai décidé d'employer une solution moins propre. Dans mon FrontController j'ai rajouté un test qui valide s'il s'agit de l'environnement de test. Si c'est le cas, il initialise un token en dur :


$auth = new AuthenticationToken('userLogin', 'userPasswordHash', 'Provider', array('ROLE_ADMIN'));
$auth->setAttributes(array('id' => 1, 'email' => 'connected@test.com'));
$secContext = $this->container->get('security.context');
$secContext->setToken($auth);


Egalement, pour accélérer l'interprétation des réponses, j'ai décidé de retourner les messages exactes pour les tests réussis. Comme indiqué dans le paragraphe précédent, la page avec l'enregistrement réussi contient le fragment suivant "registered_successfully". Voici comment on peut le présenter dans le code de l'action :


// $this->isTest : based on Kernel environment value
if($this->isTest)
{
return new Response('registered_successfully');
}


Si vous hésitez à écrire les tests pour vos applications, décidez-vous rapidement. Plus tôt vous commencez, plus de temps vous économiserez après, quand vous allez ajouter des fonctionnalités supplémentaires. Car même si l'écriture des tests prend du temps, elle vaut la peine. Grâce à elle vous allez gagner une meilleure maitrise de l'application, surtout dans les situations où vous collaborez avec plusieurs développeurs.

Et pour ceux qui se sont déjà décidés, n'oubliez pas de ne pas transférer l'environnement de tests sur votre serveur de production. Et quand vous en êtes obligés (pour voir le fonctionnement de l'application avec la configuration du serveur de production), essayez au moins de désactiver les tests après leur première exécution. Juste, une petite mesure de sécurité.

L'article écrit en rythme de:
Slaï - Je marche libre
Bartosz KONIECZNY 20-11-2011 20:11 Symfony2
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil MySQL

Qu'est-ce que signifie l'attribut UNSIGNED ?

Si notre base stocke uniquement les valeurs supérieures à 0, il faut utiliser un attribut qui aggrandit le nombre des chiffres possibles à sauvegarder. Il s'agit de l'attribut UNSIGNED qu'on peut appliquer aux champs numériques. Prenons l'exemple d'un champ type BIGINT. Normalement il occupe 8 octets et stocke les nombres entiers entre -9 223 372 036 854 775 808 à 9 223 372 036 854 775 807. Cependant, cela s'avère très peu utile lorsqu'on est sûr d'avoir uniquement les entiers positifs. En rajoutant l'attribut UNSIGNED à ce champ on double sa capacité de stockage. Alors il est capable de contenir les chiffres de 0 à 18 446 744 073 709 551 615.