MageUnit : écrire des tests unitaires Magento en toute simplicité

magento_logo

Magento est solution e-commerce puissante dont la réputation n'est plus à faire. Lorsqu'il s'agit d'écrire des tests unitaires automatisés, on ne peut toutefois pas dire que Magento nous facilite la tâche. Le code natif est souvent peu testable. Les principales raisons : la grande complexité de bon nombre de méthodes et souvent l'impossibilité d'injecter des doublures de test au sein de celles-ci. Si le développeur, en écrivant du nouveau code, peut en maîtriser sa complexité, il est plus difficile pour lui de se défaire du problème d'injection de dépendances qui se posera lors de tout appel à du code natif Magento. Heureusement il existe des solutions à ce problème et MageUnit est l'une d'entre elles.

Avant de continuer, assurez-vous d'avoir les prérequis suivants :

  • Un projet Magento fonctionnel
  • Composer
  • PHPUnit
  • Des connaissances en tests unitaires sont un plus

Le problème de l'injection des dépendances

Pour bien comprendre l'un des problèmes que l'on rencontre en voulant tester du code Magento, nous allons prendre comme exemple la méthode getPageUrl() de la classe Mage_Cms_Helper_Page.

public function getPageUrl($pageId = null)
{
    $page = Mage::getModel('cms/page');
    if (!is_null($pageId) && $pageId !== $page->getId()) {
        $page->setStoreId(Mage::app()->getStore()->getId());
        if (!$page->load($pageId)) {
            return null;
        }
    }

    if (!$page->getId()) {
        return null;
    }

    return Mage::getUrl(null, array('_direct' => $page->getIdentifier()));
}

Cette méthode prend en paramètre l'identifiant d'une page, instancie un modèle de page, charge la page correspondante à l'identifiant puis retourne son URL en s'appuyant sur son champ identifier.

La dépendance de ce code est le modèle de page CMS duquel on extrait une donnée. Si ce modèle avait été directement passé en paramètre à la méthode, cela aurait permis d'injecter une doublure permettant de vérifier le comportement de cette méthode. Malheureusement, nous sommes obligés de l'appeler en passant par l'identifiant d'une véritable page présente en base de données afin que la méthode exécute l'ensemble du code pour finalement appeler Mage::getUrl().

La difficulté, vous l'aurez compris, se situe dans le fait qu'il nous est impossible de paramétrer ce que renvoie le code Mage::getModel('cms/page') et que nous ne pouvons donc pas contrôler le comportement de la méthode à tester.

Impossible n'est en réalité pas le terme exact, mais nativement il est très compliqué d'y parvenir et cela rend l'écriture de tests fastidieuse. MageUnit résout ce problème en permettant, entre autres, de configurer le retour des méthodes de fabrication (Mage::getModel(), Mage::helper(), Mage::getSingleton()...).

Installer MageUnit

Pour installer MageUnit, rien de plus simple. Nous utiliserons Composer. Créez un fichier composer.json à la racine de votre projet avec le contenu suivant :

{
  "repositories": [
    {
      "type": "vcs",
      "url": "https://github.com/sowebdev/mageunit"
    }
  ],
  "require": {
    "sowebdev/mageunit": "dev-master"
  }
}

Puis dans votre terminal :

composer install

Vous devriez avoir une sortie qui ressemble à ceci :

Loading composer repositories with package information
Installing dependencies (including require-dev)       
  - Installing composer/installers (v1.0.21)
    Loading from cache

  - Installing sowebdev/mageunit (dev-master 51851f4)
    Cloning 51851f47e7c5423f48b5227e7c30bdd200cb94c7

Writing lock file
Generating autoload files

Bien, vous pouvez maintenant vérifier si MageUnit fonctionne avec votre installation de Magento. Pour cela nous allons lancer les tests MageUnit :

cd lib/mageunit/tests
phpunit

La sortie devrait vous indiquer que tous les tests ont été passés avec succès :

PHPUnit 4.1.4 by Sebastian Bergmann.

Configuration read from /home/magento/lib/mageunit/tests/phpunit.xml.dist

...................

Time: 204 ms, Memory: 13.25Mb

OK (19 tests, 22 assertions)

Mise en place de l'environnement de test

Dans le cadre de cet article nous mettrons en place une suite de tests dans un répertoire dev/tests/unit. Commencez par créer cette arborescence. Il nous faut ensuite un fichier de configuration pour PHPUnit et un fichier de démarrage bootstrap.php. Concernant ce dernier fichier, nous reprendrons celui de MageUnit en le copiant dans notre répertoire dev/tests/unit (vous êtes libres de le modifier pour l'adapter à vos besoins si vous savez ce que vous faites). Voici son contenu :

<?php
if (!getenv('MAGE')) {
    throw new Exception('Path to Mage.php has not been configured');
}
require getenv('MAGE');

require 'MageUnit' . DIRECTORY_SEPARATOR . 'Autoload.php';

MageUnit_Autoload::enable();

Mage::app();
Mage_Core_Model_Resource_Setup::applyAllUpdates();

Comme vous pouvez le voir, nous chargeons le fichier Mage.php dont l'emplacement doit être défini dans la variable d'environnement MAGE. C'est ensuite l'autoloader de MageUnit qui est chargé et activé avant de finalement initialiser l'application et lancer la mise à jour de la base de données si nécessaire.

Au même niveau, créez ensuite un fichier phpunit.xml.dist avec le contenu suivant :

<phpunit bootstrap="bootstrap.php" colors="true">
    <php>
        <!-- Define path to framework and Mage class -->
        <includePath>../../../lib/mageunit/src</includePath>
        <env name="MAGE" value="../../../app/Mage.php"/>
    </php>
    <listeners>
        <!-- Define test listener which will enable us to mock application parts -->
        <listener class="MageUnit_Framework_TestListener"/>
    </listeners>
    <testsuites>
        <testsuite name="Unit tests">
            <directory>testsuite</directory>
        </testsuite>
    </testsuites>
</phpunit>

Cette configuration indique d'amorcer l'exécution avec le fichier bootstrap.php. Celui-ci contient le minimum de code nécessaire à la préparation de l'environnement. Il va, entre autres, s'appuyer sur la configuration de l'include_path et de la valeur de la variable d'environnement MAGE définie dans cette configuration pour charger les fichiers nécessaires à l'exécution.

Nous avons également défini une suite de tests se trouvant dans un répertoire testsuite qu'il vous faut créer.

Enfin, et c'est une des choses les plus importantes, nous indiquons via le noeud listener la classe qui écoute les événements émis par PHPUnit à chaque étape d'exécution des tests. Cette classe du framework est chargée de remplacer certains éléments à l'exécution afin de nous permettre d'accéder aux fonctionnalités du framework pour nos tests unitaires.

Nous sommes prêts pour écrire notre premier test.

Premier test

Pour commencer, nous allons reproduire dans notre répertoire testsuite, l'arborescence du fichier testé. Cela nous permet de conserver une certaine cohérence dans l'organisation du code. Créez donc le fichier PageTest.php en respectant l'arborescence suivante :

app
|--code
   |--core
      |--Mage
         |--Cms
            |--Helper
               |--PageTest.php

Le fichier PageTest.php aura le contenu suivant :

<?php
/**
 * Test of class Mage_Cms_Helper_Page
 */
class Mage_Cms_Helper_PageTest extends MageUnit_Framework_TestCase
{
    /**
     * @var Mage_Cms_Helper_Page
     */
    protected $_subject;

    /**
     * Initialize test
     */
    protected function setUp()
    {
        $this->_subject = new Mage_Cms_Helper_Page();
    }

    /**
     * Terminate test
     */
    protected function tearDown()
    {
        $this->unsetModel('core/url');
        $this->unsetModel('cms/page');
    }

    /**
     * @covers Mage_Cms_Helper_Page::getPageUrl
     */
    public function testGetPageUrl()
    {
        $identifier = 'myId';
        $pageId = 3;

        $page = new Mage_Cms_Model_Page();
        $page->setIdentifier($identifier);
        $page->setId($pageId);
        $this->setModel('cms/page', $page);

        $urlModelMock = $this->getMockBuilder('Mage_Core_Model_Url')
            ->setMethods(array('getUrl'))
            ->getMock();
        $urlModelMock->expects($this->once())
            ->method('getUrl')
            ->with(null, array('_direct' => $identifier));
        $this->setModel('core/url', $urlModelMock);

        $this->_subject->getPageUrl($pageId);
    }
}

Premièrement vous remarquerez que nous étendons la classe MageUnit_Framework_TestCase. Ceci nous permet d'accéder aux méthodes utilitaires que nous met à disposition le framework. La méthode setUp() crée une nouvelle instance de la classe à tester qui sera utilisée dans notre méthode de test.

Jetons un oeil à la méthode de test testGetPageUrl(). Nous créons et configurons une nouvelle instance de page Mage_Cms_Model_Page. Nous l'associons ensuite à l'alias "cms/page" via la méthode $this->setModel() qui nous est fournie par MageUnit. Concrètement cela veut dire que les prochaines fois que Mage::getModel() sera appelé avec cet alias en paramètre, c'est exactement l'objet que nous avons configuré qui sera retourné. Nous pouvons donc injecter des dépendances n'importe où dans le code.

Il faut cependant être conscient que pour chaque alias pour lequel nous définissons une doublure, la fabrique n'instancie plus de nouveaux objets et ne fait que renvoyer une seule et unique doublure. C'est pourquoi il est important de réinitialiser l'alias à la fin de chaque test, dans la méthode tearDown(). Sinon vos tests partageraient éventuellement des doublures sans que vous le souhaitiez et cela pourrait en faire échouer certains.

La dernière partie de notre méthode de test est intéressante puisqu'elle nous permet de définir un simulacre (mock) dont l'alias sera "core/url" et qui va vérifier si sa méthode getUrl() est appelée avec les paramètres attendus. Cela est possible car Mage::getUrl() va en réalité appeler Mage::getModel('core/url')->getUrl(). C'est aussi simple que cela.

Lancez phpunit, vous devriez avoir la sortie suivante :

PHPUnit 4.1.4 by Sebastian Bergmann.

Configuration read from /home/magento/dev/tests/unit/phpunit.xml.dist

.

Time: 809 ms, Memory: 13.00Mb

OK (1 test, 1 assertion)

Félicitations, vous avez écrit votre premier test avec MageUnit. Vous pouvez maintenant tester votre propre code.

A vous de tester

Je vous ai présenté le framework de test MageUnit qui résout le problème d'injection de dépendances au sein du code Magento dans vos tests unitaires. Sachez qu'il existe aussi d'autres solutions. EcomDev_PHPUnit est probablement la plus complète et la plus populaire. Elle nécessite aussi un peu plus d'apprentissage et n'est pas forcément adaptée à tous les projets (notamment si le fonctionnement natif de Magento a été fortement détourné). Mais vous saurez au moins que plusieurs solutions s'offrent à vous. Je vous souhaite bonne chance pour vos tests.

Un commentaire

  1. Merci pour ce post.
    Je signale que Sören est l’auteur de ce framework MageUnit.
    Bravo super boulot !

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.