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 :
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 :
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 :
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 :
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 :
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 :
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 :
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.
Merci pour cet article sur AspectMock, je ne pensais pas pouvoir m’en servir sur M1…
Codeception fait des supers outils pour PHP.
A la place de Behat, Codeception (avec PhantomJS et non Selenium) marche super bien, je m’en sers sur pas mal de projets en production, j’ai mis en place un repository avec le squelette de ce que j’utilise en privé ici : https://github.com/opengento/codeception-magento-1.9.1.1
Merci Sören !