Tester Magento 1.x avec AspectMock

magento_logo

Magento 1.x est réputé pour être difficilement testable, principalement à cause de l'absence d'un véritable système d'injection de dépendances. Il est donc difficile de mettre en place les prérequis pour chaque test. Cette problématique était d'ailleurs la raison pour laquelle j'avais créé MageUnit. Depuis, je suis tombé sur un framework de mock très intéressant nommé AspectMock. Nous allons voir comment il rend testable quasiment n'importe quelle base de code et plus particulièrement du code Magento.

AspectMock est un framework dédié à la création de doublures de test qui s'appuie sur la librairie de programmation orientée aspect Go-AOP. Cette méthode de programmation permet d'intercepter l'exécution de code à différents niveaux et d'effectuer des opérations complémentaires autour de ce code. AspectMock permet ainsi de redéfinir des méthodes statiques et n'importe quelle méthode ou fonction utilisateur. Il est donc possible de créer des doublures de test pour des éléments qu'il n'était pas possible de bouchonner avec un code PHP traditionnel.

AspectMock est compatible avec PHP 5.4+ donc assurez-vous de disposer au minimum de cette version de PHP pour votre projet Magento. Il vous faudra aussi PHPUnit pour l'exécution des tests. Nous allons installer AspectMock en utilisant Composer. A la racine de votre projet, créez un fichier composer.json avec le contenu suivant :

{
  "require-dev": {
    "codeception/aspect-mock": "*"
  },
  "autoload-dev": {
    "psr-0": {
      "": [
        "app",
        "app/code/local",
        "app/code/community",
        "app/code/core",
        "lib"
      ]
    }
  }
}

Nous déclarons ainsi avoir besoin d'AspectMock dans sa dernière version disponible (libre à vous de préciser une version ou non). La rubrique autoload-dev nous permet d'indiquer les règles d'autoloading pour Magento car nous allons utiliser Composer non seulement pour récupérer nos dépendances mais aussi pour charger les classes Magento qui seront utilisées pendant les tests.

Dans votre terminal, utilisez ensuite la commande composer install ou composer update pour que les dépendances soient installées et que l'autoloader soit généré.

Nous allons maintenant mettre en place la structure pour nos tests. Assurez-vous de créer un répertoire dev/tests/unit. Nous y placerons nos tests et la configuration pour PHPUnit. Il nous faut créer un répertoire testsuite qui contiendra nos tests, un fichier phpunit.xml.dist qui contiendra la configuration pour PHPUnit et bootstrap.php pour initialiser l'environnement de test.

Voici le contenu de phpunit.xml.dist :

<phpunit bootstrap="bootstrap.php" colors="true" backupGlobals="false">
    <testsuites>
        <testsuite name="Unit tests">
            <directory>testsuite</directory>
        </testsuite>
    </testsuites>
</phpunit>

L'élément important ici est la directive backupGlobals qui doit valoir false. Si vous omettez cet élément, PHPUnit tentera de faire une sauvegarde et restauration des variables globales au début et à la fin de chaque test. Cela est incompatible avec AspectMock et vous aurez l'erreur suivante au moment d'exécuter PHPUnit :

Exception: Serialization of 'Closure' is not allowed

Regardons maintenant à quoi doit ressembler votre fichier bootstrap.php :

<?php
$loader = require __DIR__ . '/../../../vendor/autoload.php';
$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'debug' => true,
    'excludePaths' => [__DIR__]
]);
Mage::app();

On charge l'autoloader de Composer, puis on initialise AspectMock en lui indiquant de ne pas créer d'aspects pour les éléments se trouvant dans notre répertoire de tests. Tout élément en dehors du répertoire de test sera donc intercepté et pourra être redéfini. En dernier lieu, on initialise l'application Magento.

Mettons en application un cas de test simple pour vérifier si notre configuration fonctionne comme prévu. Dans le répertoire testsuite nous créons un fichier MyTest.php avec le contenu suivant :

<?php
class MyTest extends PHPUnit_Framework_TestCase
{
    protected function tearDown()
    {
        AspectMock\Test::clean();
    }

    public function testGetModel()
    {
        AspectMock\Test::double('Mage', ['getModel' => 'yeah']);
        $this->assertEquals('yeah', Mage::getModel('cms/page'));
    }
}

Si vous exécutez ensuite la commande phpunit dans le répertoire dev/tests/unit, vous devriez voir que le test a été exécuté avec succès.

Dans la méthode tearDown() nous faisons appel à la méthode AspectMock\Test::clean() pour nettoyer tout élément qui aurait été redéfini. Vous devez faire cela de manière systématique pour éviter que vos doublures persistent entre les tests.

Observons notre méthode de test testGetModel(). Nous faisons appel à la méthode AspectMock\Test::double() pour créer une doublure de test. Ce qui est intéressant c'est que nous avons passé en paramètre à cette méthode le nom d'une classe et la méthode à redéfinir. Cela a suffit à AspectMock pour modifier le comportement de Mage::getModel() qui dans notre exemple renvoie tout simplement la chaîne de caractères "yeah". Il est bien sûr possible de passer à AspectMock\Test::double() des objets mais il est plus impressionnant de constater qu'il peut agir au niveau de la définition d'une classe. De cette manière nous avons réussi à redéfinir une méthode statique sans aucun problème.

Nous avons vu comment redéfinir une méthode statique en agissant sur la définition d'une classe. Redéfinir une classe agit sur l'ensemble des instances de celle-ci, existantes ou futures. Observez l'exemple suivant :

public function testStubClassMethod()
{
    $obj1 = new Varien_Object();
    $obj2 = new Varien_Object();

    AspectMock\Test::double('Varien_Object', ['getId' => 'yeah']);

    $this->assertEquals('yeah', $obj1->getId());
    $this->assertEquals('yeah', $obj2->getId());
}

Nous créons deux instances de la classe Varien_Object, puis nous redéfinissons la méthode getId() de cette classe. Les deux assertions qui suivent vérifient que la méthode renvoie bien ce que nous avons configuré. Si vous exécutez ce test vous constaterez qu'il fonctionne. Nous avons donc pu modifier la classe Varien_Object à la volée, ce qui a eu un impact sur toutes ses instances.

Imaginons maintenant que nous ne voulions rédéfinir la méthode getId() d'un seul de nos objets. Pour cela il faut passer à AspectMock\Test::double() non pas le nom d'une classe mais un objet. AspectMock se chargera alors d'agir uniquement au niveau de cet objet :

public function testStubObjectMethod()
{
    $obj1 = new Varien_Object();
    $obj2 = new Varien_Object();

    AspectMock\Test::double($obj1, ['getId' => 'yeah']);

    $this->assertEquals('yeah', $obj1->getId());
    $this->assertNotEquals('yeah', $obj2->getId());
}

Comme vous pouvez le voir dans l'exemple ci-dessus, la deuxième assertion vérifie que la méthode getId() du second objet ne renvoie pas la valeur qui avait été configurée pour le premier. Nous pouvons donc créer des doublures de manière très fine.

Dans tous ces exemples nous avons toujours utilisé des valeurs de retour fixes. Mais il peut- être intéressant de pouvoir changer la valeur de retour d'une méthode de manière dynamique, c'est typiquement ce qu'il nous faut pour nous permettre de bouchonner différents appels à Mage::getModel(). Pour cela nous pouvons utiliser une fonction anonyme comme valeur de retour. Celle-ci prend en compte les paramètres passés à la méthode lorsqu'elle est appelée et peut retourner la valeur appropriée :

public function testConfigurableGetModel()
{
    AspectMock\Test::double('Mage', ['getModel' => function ($alias) {
        if ($alias == 'cms/page') {
            return 'yeah';
        } elseif ($alias == 'cms/block') {
            return 'yahoo';
        }
        return false;
    }]);

    $this->assertEquals('yeah', Mage::getModel('cms/page'));
    $this->assertEquals('yahoo', Mage::getModel('cms/block'));
}

La puissance d'AspectMock nous permet ainsi de créer des doublures pour presque n'importe quelle situation. Dans le cadre d'un projet Magento, cela s'avère très pratique vu les nombreux appels aux méthodes statiques de la classe Mage (Mage::getModel(), Mage::helper(), Mage::getStoreConfig(), ...) qu'il peut être nécessaire de bouchonner. J'espère qu'AspectMock peut vous aider dans la réalisation de vos tests unitaires avec Magento.

Un commentaire

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.