Using of Test Driven DevelopmentPractices for MagentoIvan ChepurnyiMagento Trainer / Lead Developer
Short OverviewMeet Magento NetherlandsWrite a testImaging how your feature should work and write a failing testWrite feature quickly for receiving passed testRefactor your code and run the test againPassRun the testsFailPassWrite the codeFailRun the tests
Conventional DevelopmentMeet Magento NetherlandsYou need to write a full functionality for seeing the resultYou think that your code worksDebuggingWith every fixed issue you may produce a new oneEven if you write unit tests after implementation it doesn’t guarantee that you detect the defect.
Test Driven DevelopmentMeet Magento NetherlandsYou don’t need to implement full functionality for seeing the resultYour colleagues can use your test as learning materialThe test proofs that your code works and can be verifiedSerious defects can be fixed on early state
Defect CostsMeet Magento NetherlandsPercentage from project development hours< 1%20%> 40%Defect found at the coding stageDefect found during QA phaseDefect found after going live
Type of TestsMeet Magento NetherlandsAutomated TestRegression TestLearning TestIntegration Test
Meet Magento NetherlandsTest in IsolationMake test simpleTest erroneous situationsTest Doubles (Fake heavy resources you don’t depend on)Main Principles
EcomDev_PHPUnitMeet Magento NetherlandsMaking possible isolation of your testEasy test data loading via Yaml fixturesData providers and expectations for reusable testsEasy way of testing configuration filesEasy way for Layouts & Controllers  integration test
Simple Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Model_Productextends EcomDev_PHPUnit_Test_Case{	/** * @test  * @loadFixture * @dataProviderdataProvider */ public function priceCalculation($productId, $storeId) {       $storeId = Mage::app()->getStore($storeId)->getId();       $product = Mage::getModel('catalog/product')                              ->setStoreId($storeId) ->load($productId);       $expected = $this->expected('%s-%s', $productId, $storeId);       $this->assertEquals($expected->getFinalPrice(), $product->getFinalPrice());       $this->assertEquals($expected->getPrice(), $product->getPrice());        }}Test Case Class
Simple Test CaseMeet Magento Netherlandseav:   catalog_product:      - entity_id: 1       type_id: simple         sku: book                website_ids:                    - usa_website                    - canada_website                  price: 12.99                status: 1  # Enabled                visibility: 4  # Visible in Catalog & Search                /websites:  # Set different prices per  website           usa_website:           special_price: 9.99         german_website:                         price: 9.99           special_price: 5.99 Yaml Fixture
Simple Test CaseMeet Magento Netherlands1-2: # Product=Book Store=USAfinal_price: 9.99  price: 12.991-3: # Product=Book Store=Canadafinal_price: 12.99   price: 12.99Yaml ExpectationYaml Data Provider-  - 1  - usa-  - 1  - canada-  - 1  - germany
Event Dispatch CheckMeet Magento Netherlandsclass EcomDev_Example_Test_Model_Cms_Pageextends EcomDev_PHPUnit_Test_Case{       // …	public function testAvailableStatuses() {             Mage::getModel(‘cms/page’)->getAvailableStatuses();	       $this->assertEventDispatched(		  ‘cms_page_get_available_statuses’              );	}}Test Case
Test DoublesMeet Magento Netherlandsclass EcomDev_PHPUnit_Tes_Case_Controllerextends EcomDev_PHPUnit_Test_Case{	protected function registerCookieStub()       {                $cookie = $this->getModelMock('core/cookie', array('set', 'delete'));                $cookie->expects($this->any())                       ->method('set')                       ->will($this->returnCallback(array($this, 'setCookieCallback‘)));                $cookie->expects($this->any())                       ->method('delete‘)                       ->will($this->returnCallback(array($this, 'deleteCookieCallback‘)));                $this->replaceByMock('singleton', 'core/cookie', $cookie);                return $this;        }}Test Case
Config Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{    //….    public function testModuleVersion()    {        $this->assertModuleCodePool('local');        $this->assertModuleDepends(‘Mage_Catalog’);        $this->assertModuleVersionGreaterThan(‘0.1.0');     }}Testing Module Nodes
Config Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{    //….    public function testClassAliasDefinitions()    {        $this->assertModelAlias('catalog/product', 'Mage_Catalog_Model_Product');        $this->assertResourceModelAlias(		'catalog/product', 		‘Mage_Catalog_Model_Resource_Eav_Mysql4_Product‘	);        $this->assertBlockAlias(		'catalog/product_list', 		'Mage_Catalog_Block_Product_List‘	);     }}Testing Class Aliases
Config Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{    //….    public function testEventObservers()    {	  $this->assertEventObserverDefined(            'frontend', 'customer_login',             'catalog/product_compare_item',             'bindCustomerLogin'        );     }}Testing Event Observer Definitions
Controller Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Controller_Main extends EcomDev_PHPUnit_Test_Case_Controller{    public function testRequest()    {        $this->dispatch('cms');        $this->assertRequestDispatched();        $this->assertRequestNotForwarded();        $this->assertRequestRoute('cms/index/index');        $this->assertRequestRouteName('cms');        $this->assertRequestControllerName('index');        $this->assertRequestControllerModule('Mage_Cms');        $this->assertRequestActionName('index');    }}Testing Request
Controller Test CaseMeet Magento Netherlandsclass EcomDev_Example_Test_Controller_Main        extends EcomDev_PHPUnit_Test_Case_Controller{    public function testLayout()    {        $this->dispatch('');        $this->assertLayoutHandleLoaded('cms_index_index');	$this->assertLayoutBlockCreated('right');        $this->assertLayoutBlockRendered('content');	 $this->assertLayoutBlockActionNotInvoked(	       'footer_links', 'addLink', '', array('Custom Title')        );        $this->assertLayoutBlockActionInvokedAtLeast(	       'footer_links', 'addLink', 4, '‘	);    }}Testing Layouts
What’s NextMeet Magento NetherlandsWrite automated tests for your moduleshttp://www.magentocommerce.com/magento-connect/Ecommerce%20Developers/extension/5717/ecomdev_phpunitKeep project healthy during its lifecycle with Continuous IntegrationRunning Daily BuildsRunning Unit Tests in 10 minutes after last commitHudson http://hudson-ci.orgphpUnderControlhttp://phpundercontrol.org/
Questions?ivan.chepurnyi@ecomdev.org

Using of TDD practices for Magento

  • 1.
    Using of TestDriven DevelopmentPractices for MagentoIvan ChepurnyiMagento Trainer / Lead Developer
  • 2.
    Short OverviewMeet MagentoNetherlandsWrite a testImaging how your feature should work and write a failing testWrite feature quickly for receiving passed testRefactor your code and run the test againPassRun the testsFailPassWrite the codeFailRun the tests
  • 3.
    Conventional DevelopmentMeet MagentoNetherlandsYou need to write a full functionality for seeing the resultYou think that your code worksDebuggingWith every fixed issue you may produce a new oneEven if you write unit tests after implementation it doesn’t guarantee that you detect the defect.
  • 4.
    Test Driven DevelopmentMeetMagento NetherlandsYou don’t need to implement full functionality for seeing the resultYour colleagues can use your test as learning materialThe test proofs that your code works and can be verifiedSerious defects can be fixed on early state
  • 5.
    Defect CostsMeet MagentoNetherlandsPercentage from project development hours< 1%20%> 40%Defect found at the coding stageDefect found during QA phaseDefect found after going live
  • 6.
    Type of TestsMeetMagento NetherlandsAutomated TestRegression TestLearning TestIntegration Test
  • 7.
    Meet Magento NetherlandsTestin IsolationMake test simpleTest erroneous situationsTest Doubles (Fake heavy resources you don’t depend on)Main Principles
  • 8.
    EcomDev_PHPUnitMeet Magento NetherlandsMakingpossible isolation of your testEasy test data loading via Yaml fixturesData providers and expectations for reusable testsEasy way of testing configuration filesEasy way for Layouts & Controllers integration test
  • 9.
    Simple Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Model_Productextends EcomDev_PHPUnit_Test_Case{ /** * @test * @loadFixture * @dataProviderdataProvider */ public function priceCalculation($productId, $storeId) { $storeId = Mage::app()->getStore($storeId)->getId(); $product = Mage::getModel('catalog/product') ->setStoreId($storeId) ->load($productId); $expected = $this->expected('%s-%s', $productId, $storeId); $this->assertEquals($expected->getFinalPrice(), $product->getFinalPrice()); $this->assertEquals($expected->getPrice(), $product->getPrice()); }}Test Case Class
  • 10.
    Simple Test CaseMeetMagento Netherlandseav:   catalog_product:     - entity_id: 1       type_id: simple       sku: book          website_ids:          - usa_website          - canada_website          price: 12.99        status: 1  # Enabled        visibility: 4  # Visible in Catalog & Search        /websites:  # Set different prices per website         usa_website:           special_price: 9.99         german_website:            price: 9.99           special_price: 5.99 Yaml Fixture
  • 11.
    Simple Test CaseMeetMagento Netherlands1-2: # Product=Book Store=USAfinal_price: 9.99 price: 12.991-3: # Product=Book Store=Canadafinal_price: 12.99 price: 12.99Yaml ExpectationYaml Data Provider- - 1 - usa- - 1 - canada- - 1 - germany
  • 12.
    Event Dispatch CheckMeetMagento Netherlandsclass EcomDev_Example_Test_Model_Cms_Pageextends EcomDev_PHPUnit_Test_Case{ // … public function testAvailableStatuses() { Mage::getModel(‘cms/page’)->getAvailableStatuses(); $this->assertEventDispatched( ‘cms_page_get_available_statuses’ ); }}Test Case
  • 13.
    Test DoublesMeet MagentoNetherlandsclass EcomDev_PHPUnit_Tes_Case_Controllerextends EcomDev_PHPUnit_Test_Case{ protected function registerCookieStub() { $cookie = $this->getModelMock('core/cookie', array('set', 'delete')); $cookie->expects($this->any()) ->method('set') ->will($this->returnCallback(array($this, 'setCookieCallback‘))); $cookie->expects($this->any()) ->method('delete‘) ->will($this->returnCallback(array($this, 'deleteCookieCallback‘))); $this->replaceByMock('singleton', 'core/cookie', $cookie); return $this; }}Test Case
  • 14.
    Config Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{ //…. public function testModuleVersion() { $this->assertModuleCodePool('local'); $this->assertModuleDepends(‘Mage_Catalog’); $this->assertModuleVersionGreaterThan(‘0.1.0'); }}Testing Module Nodes
  • 15.
    Config Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{ //…. public function testClassAliasDefinitions() { $this->assertModelAlias('catalog/product', 'Mage_Catalog_Model_Product'); $this->assertResourceModelAlias( 'catalog/product', ‘Mage_Catalog_Model_Resource_Eav_Mysql4_Product‘ ); $this->assertBlockAlias( 'catalog/product_list', 'Mage_Catalog_Block_Product_List‘ ); }}Testing Class Aliases
  • 16.
    Config Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Config_Main extends EcomDev_PHPUnit_Test_Case_Config{ //…. public function testEventObservers() { $this->assertEventObserverDefined( 'frontend', 'customer_login', 'catalog/product_compare_item', 'bindCustomerLogin' ); }}Testing Event Observer Definitions
  • 17.
    Controller Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Controller_Main extends EcomDev_PHPUnit_Test_Case_Controller{ public function testRequest() { $this->dispatch('cms'); $this->assertRequestDispatched(); $this->assertRequestNotForwarded(); $this->assertRequestRoute('cms/index/index'); $this->assertRequestRouteName('cms'); $this->assertRequestControllerName('index'); $this->assertRequestControllerModule('Mage_Cms'); $this->assertRequestActionName('index'); }}Testing Request
  • 18.
    Controller Test CaseMeetMagento Netherlandsclass EcomDev_Example_Test_Controller_Main extends EcomDev_PHPUnit_Test_Case_Controller{ public function testLayout() { $this->dispatch(''); $this->assertLayoutHandleLoaded('cms_index_index'); $this->assertLayoutBlockCreated('right'); $this->assertLayoutBlockRendered('content'); $this->assertLayoutBlockActionNotInvoked( 'footer_links', 'addLink', '', array('Custom Title') ); $this->assertLayoutBlockActionInvokedAtLeast( 'footer_links', 'addLink', 4, '‘ ); }}Testing Layouts
  • 19.
    What’s NextMeet MagentoNetherlandsWrite automated tests for your moduleshttp://www.magentocommerce.com/magento-connect/Ecommerce%20Developers/extension/5717/ecomdev_phpunitKeep project healthy during its lifecycle with Continuous IntegrationRunning Daily BuildsRunning Unit Tests in 10 minutes after last commitHudson http://hudson-ci.orgphpUnderControlhttp://phpundercontrol.org/
  • 20.