symfony

A la découverte de Symfony 2 : tests unitaires sur le modèle (PHPUnit et Doctrine 2)

Bonjour à toutes et à tous,

A l’heure où nous rédigeons ces lignes, la sortie officielle de Symfony2 est prévue pour mars 2011. Cette version de notre framework préféré fait actuellement l’objet d’une veille attentive de la part de la commmunauté des utilisateurs, et ELAO ne déroge pas à la règle. Nous avons donc commencé à manipuler la bête, et cet article, qui en appelle sans doute d’autres, n’a pas d’autre ambition que de consigner par écrit notre propre expérimentation du framework. Les voies que nous empruntons sont probablement discutables, car elles constituent davantage un parcours initiatique à la découverte du framework nouvelle mouture, qu’un mode d’emploi exempt de toute critique.

Pour débuter, nous avons concentré nos efforts sur la nouvelle philosophie de Doctrine 2 et sur la mise en oeuvre de tests unitaires du modèle à l’aide de PHPUnit.

Prérequis :
Pour réaliser les étapes décrites dans cet article, les bibliothèques et outils suivants sont supposés déjà installés et fonctionnels :

  • PHP 5.3.3
  • PHPUnit dans sa version 3.5
  • Une Sandbox de la Preview de Symfony2, configurée par vos soins pour accéder à une base de données (version 2_0_PR3)

La description de la mise en place et de la configuration de ces outils sort du cadre de cet article.
Ressources documentaires :

Pour les besoins de cet article, nous allons mettre en oeuvre un seul Entity afin de nous focaliser sur l’essentiel, à savoir, la manipulation des objets à l’aide de Doctrine 2. Noter que celui-ci permet de définir le modèle de plusieurs manières : à l’aide d’annotations saisies dans des classes PHP, ou bien au moyen de fichiers de configuration au format XML ou YML. Nous avons opté ici pour les annotations.

Nous allons commencer par créer un Bundle destiné notamment à héberger notre modèle (avec Symfony 2, tout est Bundle !). Pour cela, nous allons nous placer à la racine de notre Sandbox et lancer la commande Symfony générant un nouveau bundle :

cd ~/workspace/sandbox
hello/console init:bundle 'Bundle\ElaoBundle'
Pour connaître la liste des commandes disponibles :

 hello/console

Pour obtenir de l’aide sur une commande :

 hello/console help init:bundle

La commande init:bundle a créé dans le dossier src un sous-répertoire Bundle/ElaoBundle dans lequel nous allons rédiger le code de notre modèle. Mais avant toute chose, nous devons déclarer ce nouveau Bundle dans la méthode registerBundles du fichier HelloKernel :

# sandbox/hello/HelloKernel.php
# …
public function registerBundles()
{
$bundles = array(
new Symfony\Framework\KernelBundle(),
# ...
new Application\ElaoBundle\ElaoBundle(),
);
# ...

A présent, nous pouvons rédiger le code de notre premier Entity, et nous allons pour cela créer dans le répertoire de notre nouveau Bundle un sous-répertoire Entity :

 mkdir src/Bundle/ElaoBundle/Entity

Nous allons enregistrer notre premier Entity dans ce nouveau répertoire, et l’enrichir à l’aide d’annotations :

# sandbox/src/Bundle/ElaoBundle/Entity/Actor.php
<!--?php </p-->

namespace Bundle\ElaoBundle\Entity;

/**
* @Entity
*/


class Actor
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue(strategy="IDENTITY")
*
*/

protected $id;

/**
* @Column(type="string", length="255")
*
*/

protected $firstname;

/**
* @Column(type="string", length="255", nullable=false)
*
*/

protected $lastname;

/**
* @Column(type="integer")
*
*/

protected $birth_year ;

}

A présent, nous allons lancer la commande suivante :

 hello/console doctrine:generate:entities

Si vous observez le code de notre fichier Actor.php, vous remarquerez que Doctrine a généré les accesseurs (méthodes get et set) pour chacune des propriétés que nous avons saisies dans cette classe (noter toutefois que pour le champ id, seule la méthode get a été générée). Forts de cette première expérience, nous allons pouvoir générer les tables en base de données (nous considérons que la base de données existe déjà ; dans le cas contraire, il faut utiliser la commande doctrine:database:create) :

 hello/console doctrine:schema:create

A présent, nous disposons d’un Entity et d’une base de données correctement configurée, nous allons donc essayer d’enregistrer des données en base à l’aide de l’ORM. Comme nous découvrons le framework et que nous souhaitons également mettre en oeuvre PHPUnit, nous allons faire des manipulations d’entités au moyen de tests unitaires. Or, pour communiquer avec la base de données, la nouvelle version de Doctrine 2 nous oblige à passer par des Entity Managers. Toute la difficulté pour nous est d’obtenir un Entity Manager dans une application de type “console” (depuis les contrôleurs de Symfony 2, il est aisé d’obtenir un Entity Manager puisque les contrôleurs disposent d’une instance d’Entity Manager obtenue par injection de dépendances par le biais du $container (instance de SymfonyComponentDependencyInjectionContainer), ce dernier faisant en quelque sorte office de “registre des instances obtenues par injection de dépendance” : $em = $this['doctrine.orm.entity_manager']).

Dans le cadre des tests unitaires, nous allons passer par une instance de HelloKernel pour obtenir un Entity Manager (également au moyen de sa propriété $container). C’est parti, voici le contenu d’un exemple de fichier de tests (qui nécessite de créer un sous-répertoire Tests dans notre Bundle ElaoBundle) :

<!--?php </p-->

# sandbox/src/Bundle/ElaoBundle/Tests/ActorEntityTest.php

namespace Bundle\ElaoBundle\Tests;
require_once __DIR__.'/../../../../hello/HelloKernel.php';

use Bundle\ElaoBundle\Entity\Actor ;

class ActorEntityCase extends \PHPUnit_Framework_TestCase
{
/**
* @var \Doctrine\ORM\EntityManager
*/

private $em;

public function __construct()
{
$kernel = new \HelloKernel('dev', true);
$kernel-&gt;boot();
$this-&gt;em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');
}

/**
* Enter description here ...
* @return \Doctrine\ORM\EntityManager
*/

protected function getEntityManager ()
{
return $this-&gt;em;
}

public function testNewActor()
{
$em = $this-&gt;getEntityManager() ;
$actor = new Actor() ;
$actor-&gt;setFirstname('Patrick') ;
$actor-&gt;setLastname('Dewaere') ;
$actor-&gt;setBirthYear(1947) ;
$em-&gt;persist($actor) ;
$em-&gt;flush() ;

$query = $em-&gt;createQuery('SELECT COUNT(act.id) FROM \Bundle\ElaoBundle\Entity\Actor act');
$count = $query-&gt;getSingleScalarResult();

$this-&gt;assertEquals($count, 1) ;
}

}

Lançons le test pour vérifier que tout s’est correctement déroulé :

 phpunit src/Bundle/ElaoBundle/Tests/ActorEntityTest.php

Le test passe avec succès. Vérifions également à l’aide d’une commande DQL Doctrine que le contenu en base de données est conforme à nos attentes :

 hello/console doctrine:query:dql "select act.firstname, act.lastname from \Bundle\ElaoBundle\Entity\Actor act"

Nous venons donc d’écrire notre premier test unitaire sur Doctrine2 au sein d’une application Symfony 2. Cela étant, un problème subsiste : si nous lançons le test une seconde fois, nous serons confrontés à un échec puisque le nombre d’enregistrement en base de données augmente avec chaque test. Il nous faut trouver un moyen d’isoler nos données de tests ; nous pourrions soit nettoyer la base de données à l’issue du test, soit repartir d’une base “blanche” au début des tests. Nous allons opter pour la seconde solution (car elle nous permet de constater l’état de notre base de données à l’issue du test). Nous allons tout simplement invoquer les commandes doctrine:schema:drop et doctrine:schema:create dans la méthode setUp de notre test. Pour cela, nous devons obtenir une instance de SymfonyBundleFrameworkBundleConsoleApplication. Commençons donc par instancier cette classe dans le constructeur :

# sandbox/src/Bundle/ElaoBundle/Tests/ActorEntityTest.php

# ...

public function __construct()
{
$kernel = new \HelloKernel('dev', true);
$kernel-&gt;boot();
$this-&gt;em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');
$this-&gt;application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
$this-&gt;application-&gt;setAutoExit(false);
}

# ...

Nous ajoutons ensuite une méthode protégée nous permettant de lancer des commandes (comme nous envisageons de lancer deux commandes distinctes, une pour supprimer la base, l’autre pour la récréer, autant factoriser le code dans une méthode propre) ; cette méthode utilise l’instance de ConsoleApplication que nous avons instanciée dans le constructeur :

# sandbox/src/Bundle/ElaoBundle/Tests/ActorEntityTest.php
# …

protected function runConsole($command, Array $options = array())
{
$options = array_merge($options,array('command' =&gt; $command));
return $this-&gt;application-&gt;run(new \Symfony\Component\Console\Input\ArrayInput($options));
}

# ...

Enfin, nous allons compléter la méthode setUp de notre classe de test afin d’invoquer les commandes de suppression et de regénération de la base de données avant le lancement des tests :

# sandbox/src/Bundle/ElaoBundle/Tests/ActorEntityTest.php
# …

/**
* Prepares the environment before running a test.
*/

protected function setUp ()
{
parent::setUp();
$this-&gt;runConsole('doctrine:schema:drop', array( '--force' =&gt; true));
$this-&gt;runConsole('doctrine:schema:create');

}

# ...

Voilà, si nous relançons le test, nous obtenons le résultat suivant :

PHPUnit 3.5.5 by Sebastian Bergmann.

Dropping database schema...
Database schema dropped successfully!
Creating database schema...
Database schema created successfully!
.

Time: 0 seconds, Memory: 9.00Mb

OK (1 test, 1 assertion)

Conclusion

Nous avons vu comment rédiger un test unitaire “attaquant” la base de données sous Symfony2. En l’état actuel du framework, Doctrine 2 semble avoir un peu d’avance sur Symfony2 en termes de fonctionnalités et de documentation. C’est la raison pour laquelle nous avons axé nos premiers efforts sur le modèle, et eu recours aux tests unitaires pour “faire tourner” Doctrine 2, et nous affranchir ainsi des couches Contrôleurs et Vues. Il reste toutefois de nombreux points qui n’ont pas été abordés dans cet article, s’agissant du modèle et des tests unitaires. En particulier, les prochains articles aborderont sans doute les Repositories de Doctrine 2 destinés notamment à l’écriture de code permettant d’interroger la base de données, les relations dans le modèle, et la configuration de PHPUnit pour lancer une série de tests. Stay tuned !

Related posts:

11 thoughts on “A la découverte de Symfony 2 : tests unitaires sur le modèle (PHPUnit et Doctrine 2)

  1. Pingback: A la découverte de Symfony2 (Seconde partie) : allons un peu plus loin avec le modèle | Le blog technique de la team ELAO

  2. Pingback: Démarrer avec Symfony 2 | Aide mémoire

  3. Merci pour cet article.
    Cependant je le trouve assez obscur pour les débutants.
    Même si on a une grande expérience en PHP et POO on est perdu.
    Cet article est certainement très bien pour des lecteurs qui connaissent déja bien le sujet.
    Cela dit, c’est un défaut qu’on retrouve – malheureusement – dans beaucoup d’articles.

  4. Vous auriez une idée de comment executer une methode dans un controller en ligne de commande?

  5. Bonjour à tous,

    @Gildas : pour vous répondre, il faudrait savoir dans quel but vous souhaitez exécuter une méthode d’un contrôleur ; je serais tenté de répondre de la manière suivante : si c’est dans le cadre de tests, via un test fonctionnel (cf . http://symfony.com/doc/2.0/book/testing.html ). Dans le cas contraire, j’aurais tendance à penser que la logique contenue dans la méthode du contrôleur devrait être déportée dans le modèle pour pouvoir être réutilisée, notamment en ligne de commande.

    Voilà, désolé pour le manque de clarté, mais il est vraiment difficile de répondre à cette question sans connaissance du contexte.

    Cordialement, Xav

  6. @Carole : vraiment désolé si l’article vous semble un peu abscons … Cet article n’a pas vocation à exclure les débutants 8-)), mais propose un éclairage supplémentaire par rapport à la documentation officielle existante de Sf2 et Doctrine, en décrivant une manière possible d’expérimenter conjointement PhpUnit, Doctrine2 et Symfony2.

    S’il existe des points peu documentés ou qui méritent selon vous un éclaircissement, n’hésitez pas à les mentionner ; je m’efforcerais alors, dans la mesure de mes compétences, d’y répondre, soit au moyen d’un commentaire, soit en rédigeant un nouvel article.

    Cordialement, Xav

  7. Merci pour votre réponse, ce que je cherchais à faire était simplement de lancer un script en ligne de commande, en analysant le code j’ai compris comment faire. Maintenant je suis en train de faire des recherches pour effectuer des tests unitaires et fonctionnels et votre lien en commentaire et le bienvenue.
    Merci

  8. Pingback: Wojciech Sznapka » Blog Archive » Fully isolated tests in Symfony2

  9. Merci pour cet article intéressant.

    En revanche, je ne suis pas sur que supprimer / récréer la base avant CHAQUE test soit une solution optimale pour plusieurs raisons:
    - le TEMPS. Les tests permettent d’en gagner. Mais s’il faut compter 1/4h à chaque lancement uniquement parce que chaque test (sur un modèle en tout cas) va supprimer / récréer la base de données, c’est un peu long. :/
    - les tests eux-mêmes peuvent également en pâtir. Repartir à chaque fois d’une BDD vide peut ne pas être une bonne solution lorsque le projet s’appuie par exemple sur une BDD existante avec un jeu de données conséquent. Il serait beaucoup plus intéressant de pouvoir lancer chaque test sur une BDD contenant des données (mais les mêmes pour chaque test, bien entendu). Et bien sur, recharger un dump pour chaque test n’est pas une bonne solution.

    Peut-être pourrait-il être plus intéressant de surcharger la récupération de l’EntityManager de manière à faire un rollback à chaque fois?

    Sur un de nos projets actuels, nous avons étendu WebTestCase avec ces méthodes:

    /**
    * Initialize Kernel
    */
    protected function initializeKernel()
    {
    if (null === self::$isolatedKernel) {
    self::$isolatedKernel = $this->createKernel();
    self::$isolatedKernel->boot();
    }
    }

    /**
    * @return Doctrine\ORM\EntityManager
    */
    public function getEntityManager()
    {
    $this->initializeKernel();

    if (null === self::$em) {
    self::$em = self::$isolatedKernel->getContainer()->get(‘doctrine.orm.default_entity_manager’);
    } else {
    self::$em->getConnection()->rollback();
    }

    self::$em->getConnection()->beginTransaction();

    return self::$em;
    }

    Dans nos tests, nous n’avons donc plus qu’à récupérer notre EM ($this->getEntityManager()) qui sera automatiquement créé s’il n’existe pas. Dans le cas contraire, un rollback permet de repartir avec des données “propres”.

    Seul inconvénient, cette solution de fonctionne pas avec des tests fonctionnels (sur des Controllers par exemple) puisque pour ces derniers, le fonctionnement est assez différent (du à la gestion des clients qui n’est pas nécessaires dans test unitaires).
    Pour des tests unitaires en revanche, cette solution permet de rendre indépendants chacun des tests sans pour autant devoir supprimer / recréer la bdd.

    Bien entendu cette solution est certainement perfectible également! :)
    Donc n’hésitez pas à proposer des améliorations! :D

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>