Isoler les tests fonctionnels et unitaires dans symfony2

Edit: Voir le commentaire de Bidule pour une solution plus simple

Il est très pratique d’isoler ses tests du reste de l’application. Il est donc nécessaire de faire en sorte que la base de donnée utilisé lors des tests soit indépendante de la base utilisé lors du développement.  Cela évite de fausser ses tests si des données ont été modifier pendant le développement ainsi que d’utiliser une base fidèle aux fixtures entre chaque tests.

Cet article est issue de la combinaison des deux articles suivants:

Dans mon cas, l’article de haulynjason.net seul ne me permettait pas d’exécuter mes tests fonctionnels sur la même base que les tests unitaires. J’ai du utiliser l’astuce de l’article d’alexamdre-salome.fr.

Premièrement nous configurons de quoi éxécuter les tests dans une base sqlite, de façon a ce qu’elle soit isolé de la base de développement.

config_test.yml:

doctrine:
dbal
:
driver
: pdo_sqlite
path
: ":memory:"
memory
: true
orm
:
auto_generate_proxy_classes
: true
auto_mapping
: true

Nous créons la classe ModelTestCase a partir de laquelle seront étendus nos classes de tests (git):

<!--?php /*  * Adapt to your AppKernel.php path  */ require_once(__DIR__ . "../../../../../../app/AppKernel.php"); /**  * @see http://haulynjason.net/weblog/2012/01/fully-isolated-tests-in-symfony2/  */ class ModelTestCase extends \PHPUnit_Framework_TestCase {      protected $_kernel;   protected $_application;   protected $_container;   public function __construct()   {     $this--->_kernel = new \AppKernel("test", true);
$this-&gt;_kernel-&gt;boot();
$this-&gt;_container = $this-&gt;_kernel-&gt;getContainer();
parent::__construct();
}

protected function get($service)
{
return $this-&gt;_container-&gt;get($service);
}

public function setUp()
{
$this-&gt;_application = new \Symfony\Bundle\FrameworkBundle\Console\Application($this-&gt;_kernel);
$this-&gt;_application-&gt;setAutoExit(false);

$this-&gt;runConsole("doctrine:schema:drop", array("--force" =&gt; true));
$this-&gt;runConsole("doctrine:schema:create");
$this-&gt;runConsole("cache:warmup");
$this-&gt;runConsole("doctrine:fixtures:load");
}

protected function runConsole($command, Array $options = array())
{
$options["-e"] = "test";
$options["-q"] = null;
$options = array_merge($options, array('command' =&gt; $command));
return $this-&gt;_application-&gt;run(new \Symfony\Component\Console\Input\ArrayInput($options));
}
}

A partir d’ici vos classes de tests unitaires étendus de cette classes utiliserons la base sqlite pour exécuter ses tests. Passons maintenant aux tests fonctionnels: définissons le paramètre test.client.class afin d’écrire notre propre classe Client:

config.yml:

parameters:
test.client.class
: MyApp\CoreBundle\lib\Test\Client
<!--?php namespace MyApp\CoreBundle\lib\Test; use Symfony\Bundle\FrameworkBundle\Client as BaseClient; class Client extends BaseClient {   static protected $connection;   protected $requested;   protected function doRequest($request)   {       if ($this--->requested) {
$this-&gt;kernel-&gt;shutdown();
$this-&gt;kernel-&gt;boot();
}

$this-&gt;injectConnection();
$this-&gt;requested = true;

return $this-&gt;kernel-&gt;handle($request);
}

protected function injectConnection()
{
if (null === self::$connection) {
self::$connection = $this-&gt;getContainer()-&gt;get('doctrine.dbal.default_connection');
} else {
if (! $this-&gt;requested) {
self::$connection-&gt;rollback();
}
$this-&gt;getContainer()-&gt;set('doctrine.dbal.default_connection', self::$connection);
}

if (! $this-&gt;requested) {
self::$connection-&gt;beginTransaction();
}
}
}

Nous écrivons la classe WebTestCase a partir de laquelle nous étendrons nos classe de test fonctionnels:

<!--?php namespace MyApp\CoreBundle\lib\Test; use MyApp\CoreBundle\lib\Test\ModelTestCase; class WebTestCase extends ModelTestCase {      /**    *    * @param array $server    * @return Symfony\Bundle\FrameworkBundle\Client     */   protected function createClient(array $server = array())   {       $client = $this--->get('test.client');
$client-&gt;setServerParameters($server);

return $client;
}

}

Et voilà, nos tests fonctionnels seront étendu de WebTestCase, nos tests unitaires seront eux étendus de ModelTestCase et ils seront totalement isolé de la base de dév puisque ils travailleront avec une base indépendante. Les performances de vos tests seront affectés puisque avec cette méthode chaque test (fichier, pas fonction) effectue une rechargement de la base.

Symfony2: Testing: Le gestionnaire d’entité « perd » un objet

Lors de la rédaction de test fonctionnels sous symfony2 je suis souvent confronté a une erreur du gestionnaire d’entités. Alors que tout ce passe correctement en développement et en production, pendant le test fonctionnel je me retrouve parfois avec cette erreur:

A new entity was found through the relationship ‘Muzich\CoreBundle\Entity\Element#owner’ that was not configured to cascade persist operations for entity: bux. Explicitly persist the new entity or configure cascading persist operations on the relationship. If you cannot find out which entity causes the problem implement ‘Muzich\CoreBundle\Entity\User#__toString()’ to get a clue.

Le problème se résout en récupérant la dite entité par le biais de doctrine dans la même fonction que la ou se situe le ->persist().

if ($this->container->getParameter('env') == 'test')
{
  $user = $this->getDoctrine()->getRepository('MuzichCoreBundle:User')->findOneById(
    $this->container->get('security.context')->getToken()->getUser()->getId(),
    array()
  )->getSingleResult();
}

$em = $this->getDoctrine()->getEntityManager();
[...] // $user intervient dans une entité persistante
$em->flush();

Ici le paramètre ‘env’ est un paramètre personnalisé en fonction de l’environnement (config_dev.yml, config_test.yml …).

Symfony2 Tests fonctionnels: obtenir le token pour une requête ajax

J’ai eu énormément de mal a trouver un moyen de connaître le token CSRF de mon formulaire pour effectuer (lors de tests fonctionnels) une requête ‘ajax’.

$crawler = $this->client->request(
 'POST',
 $url,
 array(
   'element_add' => array(
      '_token' => '?????',
      'name'   => 'bla bla'
   ),
    array(),
    array('HTTP_X-Requested-With' => 'XMLHttpRequest')
  );

En effet dans ce code il nous manque le token de protection CSRF. Dans le soucis de ne pas modifier le code du logiciel pour permettre l’exécution du test j’ai cherché un moyen de connaître ce token dans mon test.

Ce ne fut pas une mince affaire. Après avoir cherché une méthode ou un moyen de récupérer ce token d’une manière prévu a cet effet, un utilisateur (Beryllium, merci encore) sur le chat de symfony (freenode, #symfony) m’a conseillé de regarder du coté du crawler.

Au bout du compte j’ai tout simplement récupérer le token .. dans la réponse de la première requête car le formulaire en question s’y trouve, et donc le token aussi.

$extract = $this->crawler->filter('input[name="element_add[_token]"]')
   ->extract(array('value'));
$csrf_token = $extract[0];

Symfony2: Connaitre l’environnement dans twig

Il est parfois utilise d’influencer votre template en fonction de l’environnement en cours. Dans mon cas, j’utilise un script javascript afin de gérer une liste de tags sur l’un de mes projets. Le principe du script est d’ajouter dans le dom une checkbox coché (et caché) identifiant le tag choisis.
Ce qui pose problème lorsque vous souhaitez tester votre formulaire car la manipulation du formulaire nécessite a priori la présence des champs dans le dom:

$form['element_search_form[tags]['.$hardtek_id.']'] = $hardtek_id;
$form['element_search_form[tags]['.$tribe_id.']']   = $tribe_id;

Or dans mon cas ces cases a cocher n’existe pas dans le dom. Une des solutions est donc d’afficher ces cases a cocher lorsque nous somme en environnement de test:

  {% if  app.environment != 'test' %}    
    [...]
  {% else %}    
    {{ form_row(form.tags) }}  
  {% endif %}

Un tutoriel Français pour Symfony 2

Pour commencer ce nouveau blog, voici une nouvelle qui fera plaisir aux développeurs php non anglophones. Je viens d’apprendre  par le biais de planète libre qu’un tutoriel en Français portant sur Symfony2 est en cours de réalisation.
Ayant commencé l’utilisation de ce framework depuis quelques mois maintenant, je vous conseille d’en profiter pour l’essayer !