Les tests unitaires sous Magento 1.x - Partie 1
⏲️ ~23 min de lecture
Publié le
par
composer
Magento
modman
phpunit
tests unitaires
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><server name="MAGENTO_DIRECTORY" value="htdocs" \=""><\/php><\/phpunit>/' vendor/ecomdev/ecomdev_phpunit/phpunit.xml.dist > phpunit.xml.dist</server></php>
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
:
<config>
<modules>
<occitech_unittests>
<active>true</active>
<codepool>local</codepool>
</occitech_unittests>
</modules>
</config>
Contenu du fichier config.xml
:
<config>
<modules>
<occitech_unittests>
<version>0.0.1</version>
</occitech_unittests>
</modules>
<phpunit>
<suite>
<modules>
<occitech_unittests></occitech_unittests>
</modules>
</suite>
</phpunit>
<global>
<models>
<occitech_unittests>
<class>Occitech_UnitTests_Model</class>
</occitech_unittests>
</models>
</global>
</config>
Notez la partie avec le noeud <phpunit></phpunit>
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 :
<testsuite name="Occitech Test Suite">
<directory suffix=".php">htdocs/app/code/local/Occitech/UnitTests/Test</directory>
</testsuite>
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.
<events>
<checkout_cart_add_product_complete>
<observers>
<occitech_unittests_observer>
<class>occitech_unittests/observer</class>
<method>onProductAdded</method>
</occitech_unittests_observer>
</observers>
</checkout_cart_add_product_complete>
</events>
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.
public function onProductAdded(Varien_Event_Observer $observer)
{
$observer->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 :
class Occitech_UnitTests_Test_Model_Observer extends EcomDev_PHPUnit_Test_Case_Config
{
public function setUp()
{
$this->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.
Cet article vous a plu ? Sachez que nous recrutons !