Blog

Les tests unitaires sous Magento 1.x – Partie 1

Cette première partie explique la mise en place d’une boutique Magento avec Composer et Modman, les tests unitaires sous Magento à l’aide du module EcomDev_PHPunit, ainsi nous espérons qu’il permettra à d’autres personnes de tester leurs modules. Vous pouvez récupérer le code du projet à l’adresse suivante Magento Unit Tests

Magento est une solution e-commerce très complète et flexible, mais qui vient avec un léger inconvénient, la difficulté de base d’écrire et exécuter des tests unitaires.

Or chez Occitech, le TDD nous tient à cœur, et nous avons cherché comment écrire des tests unitaires pour Magento.

La communauté active de Magento nous offre une extension Magento pour notre plus grand plaisir : EcomDev_PHPunit. Cette extension permet ni plus ni moins de tester votre boutique dans les moindre recoins.

Installation du module

Hélas à l’heure où nous écrivons ces lignes, l’installation du module n’est pas aussi facile que le laisse penser la documentation.

Notre cas de test sera une boutique Magento 1.9.1.0 fraîchement installée via Modman et Composer.

Installation de la boutique :

Le composer.json qui va bien :

{
    "repositories": [
        {
            "type": "composer",
            "url": "http://packages.firegento.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {
        "magento-hackathon/magento-composer-installer": "2.1.1",
        "magento/core": "1.9.1.0",
        "colinmollenhour/modman": "@stable"
    },
    "require-dev": {
        "mikey179/vfsstream": "1.4.0",
        "ecomdev/ecomdev_phpunit": "0.3.7",
        "magento-hackathon/composer-command-integrator": "*"
    },
    "extra": {
        "magento-root-dir": "htdocs/",
        "magento-deploystrategy": "link",
        "magento-force": 1
    },
    "scripts": {
        "post-update-cmd": "php ./vendor/bin/composerCommandIntegrator.php magento-module-deploy",
        "post-install-cmd": "php ./vendor/bin/composerCommandIntegrator.php magento-module-deploy"
    }
}

Et installons le tout : composer install.

Paramétrage Ecomdev_PHPUnit :

Une fois internet téléchargé, il vous faudra faire plusieurs modifications :

Copier-coller le fichier local.xml.phpunit et phpunit.xml.dist au bon endroit :

cp vendor/ecomdev/ecomdev_phpunit/app/etc/local.xml.phpunit htdocs/app/etc/;
sed -e 's/app/htdocs\/app/' -e 's/lib/htdocs\/lib/' -e 's/var/htdocs\/var/' -e 's/<\/phpunit>/<\/php><\/phpunit>/' vendor/ecomdev/ecomdev_phpunit/phpunit.xml.dist > phpunit.xml.dist

Créons une base de données de test : /!\ Attention ! Il faut que la base de données de notre Magento fraîchement installée soit en place

mysql -uroot -p -e "create database magento_test;"

Exécutons le shell Ecomdev :

cd htdocs/shell
php ecomdev-phpunit.php -a magento-config --db-name magento_test --base-url http://your.magento.url/
cd -

— Et enfin exécutons les tests une première fois pour mettre en place la base de test (l’exécution peut prendre un peu de temps) :

vendor/bin/phpunit

Vous devriez avoir en sortie quelque chose comme :

OK (74 tests, 174 assertions)

Maintenant passons aux choses sérieuses :

Les tests

Écrivons notre 1er test, pour ce faire nous allons créer un premier module que l’on nommera Occitech_UnitTests. Créons le répertoire pour notre module mkdir -p .modman/Occitech_UnitTests.

Ajoutons également un fichier nommé .basedir pour éviter de répeter le répertoire de destination dans les mappings modman : echo "htdocs/" > .modman/.basedir.

Créons la structure du module :

.modman
    |_Occitech_UnitTests
        |_src
            |_app
                |_code
                    |_local
                       |_Occitech
                           |_UnitTests
                               |_etc
                                   |_config.xml
                               |_Test
                                   |_Model
                                       |_Observer.php
                |_etc
                    |_modules
                        |_Occitech_UnitTests.xml
            .modman
htdocs
vendor

Contenu du fichier Occitech_UnitTests.xml :


    
        
            true
            local
        
    

Contenu du fichier config.xml :


    
        
            0.0.1
        
    
    
        
            
                
            
        
    
    
        
            
                Occitech_UnitTests_Model
            
        
    

Notez la partie avec le noeud permettant au listener de EcomDev de venir exécuter nos futur tests.

Contenu du fichier .modman :

src/app/code/local/Occitech/UnitTests app/code/local/Occitech/UnitTests
src/app/etc/modules/* app/etc/modules/

Éditons le fichier phpunit.xml.dist pour ajouter notre répertoire pour les tests :

   
        htdocs/app/code/local/Occitech/UnitTests/Test
   

Et créeons un test simple pour nous assurer du fonctionnement de la test suite.

Contenu du fichier Observer.php :

class Occitech_UnitTests_Test_Model_Observer extends EcomDev_PHPUnit_Test_Case_Config
{
    public function testIsSet()
    {
        $this->assertTrue(true);
    }
}

Faisons un modman deploy afin de placer nos nouveaux fichiers : vendor/bin/modman deploy-all et exécutons nos tests vendor/bin/phpunit --testsuite "Occitech Test Suite" le résultat devrait être le suivant :

OK (1 test, 1 assertion)

Notre test suite est donc opérationnelle.

Nous allons donc maintenant écrire un test pour définir quel événement écoutera notre observer, dans notre cas nous allons écouter l’événement qui est émis lors de l’ajout d’un produit au panier à savoir checkout_cart_add_product_complete

Voici le test qui va couvrir cette définition :

    public function testEventCheckoutCartAddProductCompleteIsListened()
    {
        $this->assertEventObserverDefined('global', 'checkout_cart_add_product_complete', 'occitech_unittests/observer', 'onProductAdded');
    }

On ré-exécute nos tests afin de s’assurer que notre test échoue vendor/bin/phpunit --testsuite "Occitech Test Suite".

Puis on va donc modifier notre fichier config.xml et on va créer notre observer.


    
        
            
                occitech_unittests/observer
                onProductAdded
            
        
    

Le contenu de notre fichier Observer.php :


Redéployons nos fichiers, et exécutons à nouveaux tests, et la notre test passe au vert.

Maintenant nous allons tester que notre méthode onProductAdded fasse une action bien déterminée.

Pour ce faire on va créer des fixtures toutes simples default.yaml

scope:
  website: # Initialize websites
    - website_id: 1
      code: base
      name: Default
      default_group_id: 1
  group: # Initializes store groups
    - group_id: 1
      website_id: 1
      name: Default
      default_store_id: 1
      root_category_id: 2 # Default Category
  store: # Initializes store views
    - store_id: 1
      website_id: 1
      group_id: 1
      code: default
      name: France
      is_active: 1

eav:
  catalog_product:
    - entity_id: 1
      attribute_set_id: 4
      type_id: simple
      sku: "product-1"
      name: "Product 1"
      stock:
        qty: 100.00
        is_in_stock: 1
      store_ids:
        - base
      category_ids:
        - 2
      price: 10.00
      tax_class_id: 2
      status: 1
      visibility: 4
    - entity_id: 2
     attribute_set_id: 4
     type_id: simple
     sku: "gift-1"
     name: "Gift Product"
     stock:
       qty: 100.00
       is_in_stock: 1
     store_ids:
       - default
     category_ids:
       - 2
     price: 10.00
     tax_class_id: 2
     status: 1
     visibility: 4

Que l'on va mettre dans un répertoire fixtures

.modman
    |_Occitech_UnitTests
        |_src
            |_app
                |_code
                    |_local
                       |_Occitech
                           |_UnitTests
                               |_etc
                                   |_config.xml
                               |_Test
                                   |_Model
                                       |_fixtures
                                          |_default.yaml
                                       |_Observer.php

Nous allons ensuite rajouter une annotation dans notre classe de test afin de charger la fixture créer pour tous les tests.


La fonctionnalité que l'on voudra tester est la suivante :

Lorsqu'un utilisateur ajoutera un produit particulier au panier, automatiquement il aura un produit offert ajouté également.

Ci-dessous le test (basique) de la fonctionnalité :

public function testGiftIsAddedWhenProductIsAddedToCart() {
        $event = $this->generateObserver(array('quote_item' => null, 'product' => null), 'checkout_cart_product_add_after');

        $giftProduct = Mage::getModel('catalog/product')->load('2');
        $MockedCart = $this->getModelMock('checkout/cart', array('addProduct'));
        $MockedCart->expects($this->once())
            ->method('addProduct')
            ->with($this->equalTo($giftProduct))
            ->will($this->returnValue($MockedCart));

        $this->replaceByMock('model', 'checkout/cart', $MockedCart);

        $ObserverToTest = Mage::getModel('occitech_unittests/observer');
        $ObserverToTest->onProductAdded($event);
    }

Puis exécutons les tests pour vérifier que notre dernier test échoue.

Expliquons brièvement le contenu du test :

On génère un "faux" évènement qui sera utilisé par notre observeur.

Puis on va créer un Mock du model Checkout/Cart afin de tester que l'on ajoute bien au panier le produit offert.

Ne surtout pas oublier l'utilisation de $this->replaceByMock qui a pour effet de ne pas instancier le vrai model Checkout/Cart dans la méthode testée de notre observer mais de récupérer le mock précédemment défini.

Il ne nous reste plus qu'a écrire le code faisant passer ce test :

 public function onProductAdded(Varien_Event_Observer $observer)
    {
        $giftProduct = Mage::getModel('catalog/product')->load(2);
        Mage::getModel('checkout/cart')->addProduct($giftProduct);
    }

Ré-exécutons nos tests, et là vérifions que tous nos tests sont aux verts.

Bon, le problème c'est que l'argent c'est le nerf de la guerre, et que l'on veut offir ce cadeau quand dans le cas ou le produit ajouté est un produit spécifique (tant qu'à faire sur lequel on marge le plus) et éviter d'offire un produit cadeau quand on rajoute ce produit cadeau.

Nous allons écrire un test qui dans notre cas va simplement vérifier que lors de l'ajout du produit cadeau lui même, on ne rajoutera pas le cadeau.

    public function testGiftIsNotAddedIfProductIsNotTheSpecialOne()
    {
        $giftProduct = Mage::getModel('catalog/product')->load('2');
        $event = $this->generateObserver(array('quote_item' => null, 'product' => $giftProduct), 'checkout_cart_product_add_after');
        $ObserverToTest = Mage::getModel('occitech_unittests/observer');

        $MockedCart = $this->getModelMock('checkout/cart', array('addProduct'));
        $MockedCart->expects($this->never())
            ->method('addProduct')
            ->with($this->equalTo($giftProduct))
            ->will($this->returnValue($MockedCart));

        $this->replaceByMock('model', 'checkout/cart', $MockedCart);
        $ObserverToTest->onProductAdded($event);
    }

Puis exécutons les tests pour vérifier que notre dernier test échoue.

Enfin rajoutons le code dans notre observer, permettant de faire passer ce test.

getEvent()->getProduct();
        if ($this->isGiftOfferedFor($productAdded)) {
            $giftProduct = Mage::getModel('catalog/product')->load(2);
            Mage::getModel('checkout/cart')->addProduct($giftProduct);
        }
    }

    public function isGiftOfferedFor(Mage_Catalog_Model_Product $product)
    {
        return $product->getSku() === self::PRODUCT_SKU_TO_TARGET;
    }
}

Puis exécutons à nouveaux les tests, et encore une fois ils sont verts.

La partie 1 sur les tests touchent presque à sa fin, mais avant de partir, refactorons nos tests car comme on peut le voir il y a beaucoup de similarités entre les tests.

La classe de test une fois refactorée :

SUT = Mage::getModel('occitech_unittests/observer');
    }


    public function testIsSet()
    {
        $this->assertTrue(true);
    }

    public function testEventCheckoutCartAddProductCompleteIsListened()
    {
        $this->assertEventObserverDefined('global', 'checkout_cart_product_add_after', 'occitech_unittests/observer', 'onProductAdded');
    }

    public function testGiftIsAddedWhenProductIsAddedToCart() {
        $product = Mage::getModel('catalog/product')->load(1);
        $event = $this->generateAfterProductAddToCartEvent($product);

        $MockedCart = $this->getModelMock('checkout/cart', array('addProduct'));
        $this->expectGiftIsAddedToCart($MockedCart);
        $this->replaceByMock('model', 'checkout/cart', $MockedCart);

        $this->SUT->onProductAdded($event);
    }

    public function testGiftIsNotAddedIfProductIsNotTheSpecialOne()
    {
        $giftProduct = Mage::getModel('catalog/product')->load('2');
        $event = $this->generateAfterProductAddToCartEvent($giftProduct);

        $MockedCart = $this->getModelMock('checkout/cart', array('addProduct'));
        $this->expectGiftIsNotAddedToCart($MockedCart);
        $this->replaceByMock('model', 'checkout/cart', $MockedCart);

        $this->SUT->onProductAdded($event);
    }

    /**
     * Convenients methods below
     */

    private function expectGiftIsAddedToCart($MockedCart)
    {
        $this->prepareMockedCart($MockedCart, $this->once());
    }

    private function expectGiftIsNotAddedToCart($MockedCart)
    {
        $this->prepareMockedCart($MockedCart, $this->never());
    }

    private function prepareMockedCart($MockedCart, $expectation)
    {
        $giftProduct = Mage::getModel('catalog/product')->load('2');
        $MockedCart->expects($expectation)
            ->method('addProduct')
            ->with($this->equalTo($giftProduct))
            ->will($this->returnValue($MockedCart));
    }

    private function generateAfterProductAddToCartEvent(Mage_Catalog_Model_Product $product)
    {
        return $this->generateObserver(array('quote_item' => null, 'product' => $product), 'checkout_cart_product_add_after');
    }
}

Ce qui a été fait:

  • On a extrait la génération du faux évènement
  • On a également extrait la portion de code générant le mock du model Checkout/Cart
  • On a crée un attribut privé ayant une instance de notre observer pour éviter de rédéclarer le Mage::getModel('occitech_unittests/observe') pour chaque test.

Toujours en vérifiant entre chaque étape que nos tests soient verts

Maintenant la partie 1 touche à sa fin.

Ce qui est prévu pour la partie 2, l'utilisation des dataProviders et expectations dans nos tests, ainsi que le test des blocks et controlleurs.