PHPUnit et Symfony 2, mise en place

Cet article explique comment mettre en place PHPUnit dans un projet Symfony 2 et il donne quelques points importants pour la mise en place de tests unitaires.

php-unit

Mise en place de PHPUnit

Ajout de la dépendance à composer.json :

{
    "require-dev": {
        "phpunit/phpunit": "5.4.*"
    }
}

 

Installation du module "php5-xdebug" qui est nécessaire pour lancer les tests unitaires avec les options de logging.

 

Lancements des tests

Lancement de tous les tests unitaires présents dans le fichier de configuration phpunit.xml :

php vendor/phpunit/phpunit/phpunit --configuration=build/phpunit.xml

On indique l'emplacement du fichier de configuration de PHPUnit. Par défaut il sera cherché sous le nom build/phpunit.xml

NB : si vous êtes sous Windows, il faut indiquer le chemin de php pour pouvoir lancer correctement la commande php (php du serveur invoqué par défaut ; -c permet d'indiquer le php CLI (Command Line))

php -C "D:/UwAmp/bin/php/php-7.0.3/php_uwamp.ini" [...]

 

Il est possible de lancer uniquement une classe de Test en indiquant le chemin de ce test :

php vendor/phpunit/phpunit/phpunit src/Me/UploadBundle/UploadControllerTest.php --configuration=build/phpunit.xml

 

Fichier phpunit.xml

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

<phpunit
    backupGlobals               = "false"
    backupStaticAttributes      = "false"
    colors                      = "true"
    convertErrorsToExceptions   = "true"
    convertNoticesToExceptions  = "true"
    convertWarningsToExceptions = "true"
    processIsolation            = "false"
    stopOnFailure               = "false"
    syntaxCheck                 = "false"
    bootstrap                   = "../app/bootstrap.php.cache" >

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>

    <php>
        <server name="KERNEL_DIR" value="app/" />
    </php>

    <logging>
        <log type="junit" target="logs/junit.xml" logIncompleteSkipped="false"/>
    </logging>

    <filter>
        <whitelist>
            <directory>../src</directory>
            <exclude>
                <directory>../src/*/*Bundle/Resources</directory>
                <directory>../src/*/*Bundle/Tests</directory>
                <directory>../src/*/Bundle/*Bundle/Resources</directory>
                <directory>../src/*/Bundle/*Bundle/Tests</directory>
            </exclude>
        </whitelist>
    </filter>

</phpunit>

 

Les classes de Test sont présentes dans les bundles dans un dossier "Tests". S'ensuit ensuite la hiérarchie de dossiers habituelle :

  • Controller avec des fichiers *ControllerTest.php
  • Repository avec des fichiers *RepositoryTest.php
  • Entity avec des fichiers *EntityTest.php

Couverture de code

La balise "whitelist" présente dans la balise "filter" permet d'indiquer les fichiers qui seront dans le rapport de couverture de code (en anglais : code coverage reporting).

On exclu les dossiers contenant les classes de Test (../src/*/*Bundle/Tests) car ce code ne fait pas partir du code applicatif.

Il est possible de définir en dur les propriétés de l'exécution des tests ou de les passer dans la ligne de commande exécutée.

Fichier phpunit.xml :

<logging>
   <log type="coverage-html" target="tmp/report" lowUpperBound="35" highLowerBound="70" />
</logging>

Ligne de commande :

php vendor/phpunit/phpunit/phpunit --coverage-html /tmp/reports --configuration=build/phpunit.xml

Si on ne met pas les options lowUpperBound et highLowerBound, par défaut ces seuils sont à 50 et 90.

coverage-report

Exemple de code coverage report

Contenu classe de tests

Si l'on  veut tester notre contrôleur UploadController.php, il faut alors créer la classe UploadControllerTest.php en suivant la même hiérarchie donc à l'emplacement src/Me/UploadBundle/Tests/Controller/UploadControllerTest.php.

Une fonction de test doit être "publique" et commencer par "test" ; exemple : testFunctionnalityOne().

Toutes les fonctions commençant par "test" (et publiques) seront donc lancées durant l'exécution de la classe UploadControllerTest.php.

On peut faire hériter notre classe de test de :

  • WebTestCase (Symfony\Bundle\FrameworkBundle\Test\WebTestCase)
  • TestCase (PHPUnit\Framework\TestCase)

Les classes de tests sur les Controller hériteront de WebTestCase pour pouvoir faire des requêtes HTTP (accès à des pages, envoie de requêtes).

Les classes de tests sur les Repository hériteront de TestCase pour pouvoir faire des accès à la base de données avec l'EntityManager.

La classe TestCase est toute simple. Elle hérite uniquement de PHPUnit_Framework_TestCase. Il est donc préférable de créer sa propre classe "TestCase.php", d'hériter de PHPUnit_Framework_TestCase et de faire son propre contenu.

TestCase

namespace Me\UploadBundle\Tests;

class TestCase extends \PHPUnit_Framework_TestCase
{
    protected static $kernel;
    protected static $em;

    public function __construct()
    {
       static::$kernel = new \AppKernel('test', true);
       static::$kernel->boot();
       static::$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
    }

    protected static function getEntityManager()
    {
       return self::$em;
    }

    public static function setUpBeforeClass()
    {
        self::generateSchema();
        self::loadFixtures(self::$kernel, self::$em);
        parent::setUpBeforeClass();
    }

    public static function tearDownAfterClass()
    {
        static::$kernel->shutDown();
        parent::tearDownAfterClass();
    }

    protected static function generateSchema()
    {
       $metadata = self::getMetadata();

       if(!empty($metadata)) {
           $tool = new SchemaTool(self::$em);
           $tool->dropSchema($metadata);
           $tool->createSchema($metadata);
       } else {
           throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
       }
    }

    protected static function getMetadata()
    {
        return self::$em->getMetadataFactory()->getAllMetadata();
    }

    private static function loadFixtures(\AppKernel $kernel, \Doctrine\ORM\EntityManager $manager)
    {
       $loader = new Loader($kernel->getContainer());

       foreach($kernel->getBundles() as $bundle) {
           $path = $bundle->getPath().'DataFixtures/ORM';

           if(is_dir($path)) {
              $loader->loadFromDirectory($path);
           }
       }

       $fixtures = $loader->getFixtures();
       if(!$fixtures) {
          throw new InvalidArgumentException('Could not find any fixtures to load in');
       }
       $purger = new ORMPurger($manager);
       $executor = new ORMExecutor($manager, $purger);
       $executor->execute($fixtures, true);
    }
}

Avec ce code, avant l'exécution de chaque classe de tests, on va générer le schéma de BDD et remplir la table (BDD de l'environnement test : défini dans le fichier "config_test.yml" dans doctrine.dbal.dbname) avec des données (fichiers de données : DataFixtures/ORM/Load*Data.php).

En héritant de la classe PHPUnit_Framework_TestCase, on peut donc définir des actions à effectuer  avant/après chaque classe et avant/après chaque test (fonction de test)

  • setUpBeforeClass()
  • tearDownAfterClass()
  • setUp()
  • tearDown()

WebTestCase

Cette classe du Bundle de Symfony, hérite elle aussi de la classe \PHPUnit_Framework_TestCase. Avec cette dernière on peut créer un client HTTP pour pouvoir tester les routes des contrôleurs (accès, modification, création, suppression).

Dans le contrôleur, pour obtenir un client :

protected function getClient()
{
    $client = static::createClient();
    $client->followRedirects();

    return $client;
}

Une fois le client obtenu on peut faire des requêtes :

$client->request('GET', '/en/admin'); 
$status = $client->getResponse()->getStatusCode();

 

Chargement des données FixturesData (comme dans TestCase) ou utilisation d'un mock pour l'EntityManager.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.