QUALITY ASSURANCE IN                  MAGENTO EXTENSIONS                             Automated Functional TestingWednesday...
WHO AM I    • Alistair           Stead    • Technical Team             Lead @ Ibuildings UK / Session Digital    • Lead   ...
CREATING SOFTWARE IS                        COMPLEXWednesday, 9 February 2011
CREATING SOFTWARE IS                        COMPLEX                                       ing it is argua bly harder)     ...
FAULT TOLERANCE IS                                  LOWWednesday, 9 February 2011
“Hello, technical support?”                                                           “I would like                       ...
E-COMMERCE HAS A LOW              TOLERANCE TO FAULTS.Wednesday, 9 February 2011
MAGENTO MAKES IT                  OBVIOUS SOMETHING IS                        WRONGWednesday, 9 February 2011
With whom should                                   the BUCK stop?Wednesday, 9 February 2011
With whom should                                   the BUCK stop?           e sure its not us....   Lets makWednesday, 9 F...
SO WHAT CAN WE DO TO                  FIX THIS?Wednesday, 9 February 2011
We are clever                                     people right?Wednesday, 9 February 2011
Just get more of us?Wednesday, 9 February 2011
Testing at the eleventh hour...Wednesday, 9 February 2011
We can use                             technology                               to help!Wednesday, 9 February 2011
AUTOMATED TESTING        FRAMEWORKS CAN DO THE             HEAVY LIFTING.                             Selenium   WatirWedn...
PHPUNIT                 The de-facto standard for unit testing in PHP projectsWednesday, 9 February 2011
WE CAN RUN UNIT TESTS                  FOR MAGENTO!Wednesday, 9 February 2011
<?php /**  * Magento PHPUnit TestCase  *  * @package     Ibuildings_Mage_Test_PHPUnit  * @copyright   Copyright (c) 2011 I...
<?php /**  * Magento PHPUnit TestCase  *  * @package     Ibuildings_Mage_Test_PHPUnit  * @copyright   Copyright (c) 2011 I...
MODELSWednesday, 9 February 2011
<?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase {     /**      * Member variable t...
<?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase {     /**      * Member variable t...
<?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase {     /**      * Member variable t...
/**       * mageSingletonFactoryReturnsExpectedObject       * @author Alistair Stead       * @test       */     public fun...
/**       * mageSingletonFactoryReturnsExpectedObject       * @author Alistair Stead       * @test       */     public fun...
/**       * mageSingletonFactoryReturnsExpectedObject       * @author Alistair Stead       * @test       */     public fun...
/**        * getTransactionTypesReturnsArray        * @author Alistair Stead        * @test        */      public function...
/**        * getTransactionTypesReturnsArray        * @author Alistair Stead        * @test        */      public function...
HELPERSWednesday, 9 February 2011
/**          * Setup fixtures and dependencies          *          * @return void          * @author Alistair Stead       ...
/**          * Setup fixtures and dependencies          *          * @return void          * @author Alistair Stead       ...
/**          * Setup fixtures and dependencies          *          * @return void          * @author Alistair Stead       ...
/**         * helperFactoryReturnsTheExpectedClass         * @author Alistair Stead         * @test         */       publi...
/**         * helperFactoryReturnsTheExpectedClass         * @author Alistair Stead         * @test         */       publi...
/**         * helperFactoryReturnsTheExpectedClass         * @author Alistair Stead         * @test         */       publi...
Okay we’re done lets go home...Wednesday, 9 February 2011
Hold on,                              we have not really                             done anything new.Wednesday, 9 Februa...
DID WE MENTION           FUNCTIONAL TESTING YET?Wednesday, 9 February 2011
QUALITY ASSURANCE IN                  MAGENTO EXTENSIONS                             Automated Functional TestingWednesday...
ZEND_TEST?Wednesday, 9 February 2011
MAGENTO IS CLOSELY               COUPLED TO          MAGE_CORE_APPLICATIONWednesday, 9 February 2011
Coupled components cause inflexibilityWednesday, 9 February 2011
Dependency injection                                           is the keyWednesday, 9 February 2011
MAGE-TEST                             http://github.com/ibuildings/Mage-TestWednesday, 9 February 2011
Mage_Core_Model_App                      Ibuildings_Mage_Controller_Request_HttpTestCase                    Ibuildings_Mag...
MAGE_CORE_APPLICATION                             Loaded from the community code poolWednesday, 9 February 2011
/**         * Provide a public method to allow the internal Request object         * to be set at runtime. This can be use...
/**         * Provide a public method to allow the internal Request object         * to be set at runtime. This can be use...
/**        * Provide a public method to allow the protected internal Response object        * to be set at runtime. This c...
/**        * Provide a public method to allow the protected internal Response object        * to be set at runtime. This c...
CONTROLLER TESTS                   Ibuildings_Mage_Test_PHPUnit_ControllerTestCaseWednesday, 9 February 2011
/**   * Bootstrap the Mage application in a similar way to the procedure   * of index.php   *   * Then sets test case requ...
/**   * Bootstrap the Mage application in a similar way to the procedure   * of index.php   *   * Then sets test case requ...
/**   * Bootstrap the Mage application in a similar way to the procedure   * of index.php   *   * Then sets test case requ...
Ibuildings_Mage_Controller_Request_HttpTestCase                                                                           ...
Ibuildings_Mage_Controller_Request_HttpTestCase                                                                           ...
Mage_Core_Model_App               Ibuildings_Mage_Controller_Request_HttpTestCase                                         ...
<?php /**  * Mage_Catalog_IndexControllerTest  *  * @package    Mage_Catalog  * @subpackage Mage_Catalog_Test  *  *  * @us...
<?php /**  * Mage_Catalog_IndexControllerTest  *  * @package    Mage_Catalog  * @subpackage Mage_Catalog_Test  *  *  * @us...
<?php /**  * Mage_Catalog_IndexControllerTest  *  * @package    Mage_Catalog  * @subpackage Mage_Catalog_Test  *  *  * @us...
/**         * theAdminRouteAccessesTheAdminApplicationArea         * @author Alistair Stead         * @test         */    ...
/**         * theAdminRouteAccessesTheAdminApplicationArea         * @author Alistair Stead         * @test         */    ...
/**         * theAdminRouteAccessesTheAdminApplicationArea         * @author Alistair Stead         * @test         */    ...
/**         * theAdminRouteAccessesTheAdminApplicationArea         * @author Alistair Stead         * @test         */    ...
/**         * theAdminRouteAccessesTheAdminApplicationArea         * @author Alistair Stead         * @test         */    ...
/**         * submittingInvalidCredsShouldDisplayError         * @author Alistair Stead         * @group login         * @...
/**         * submittingInvalidCredsShouldDisplayError         * @author Alistair Stead         * @group login         * @...
/**         * submittingInvalidCredsShouldDisplayError         * @author Alistair Stead         * @group login         * @...
/**         * submittingInvalidCredsShouldDisplayError         * @author Alistair Stead         * @group login         * @...
/**         * submittingInvalidCredsShouldDisplayError         * @author Alistair Stead         * @group login         * @...
/**        * submittingValidCredsShouldDisplayDashboard        * @author Alistair Stead        * @group login        * @te...
/**        * submittingValidCredsShouldDisplayDashboard        * @author Alistair Stead        * @group login        * @te...
class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase {      /**        * indexActionListsOrd...
class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase {      /**        * indexActionListsOrd...
class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase {      /**        * indexActionListsOrd...
TESTING EMAIL CONTENT                   Who gets all the emails sent to test@example.com?Wednesday, 9 February 2011
public function mageBootstrap()      {          Mage::reset();          if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) {    ...
public function mageBootstrap()      {          Mage::reset();          if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) {    ...
/**        * submittingForgotPasswordWithValidEmailReturnsSuccess        * @author Alistair Stead        * @group password...
/**        * submittingForgotPasswordWithValidEmailReturnsSuccess        * @author Alistair Stead        * @group password...
/**        * submittingForgotPasswordWithValidEmailReturnsSuccess        * @author Alistair Stead        * @group password...
/**        * submittingForgotPasswordWithValidEmailReturnsSuccess        * @author Alistair Stead        * @group password...
SO WHAT DO AUTOMATED              TESTS GIVE US?Wednesday, 9 February 2011
CODE COVERAGEWednesday, 9 February 2011
STATIC CODE ANALYSISWednesday, 9 February 2011
CONTINUOUS INTEGRATIONWednesday, 9 February 2011
Wednesday, 9 February 2011
Wednesday, 9 February 2011
SO WHAT NOW?Wednesday, 9 February 2011
MAGE-TEST IS OPEN SOURCE    • Use          Mage-Test to test your development projects    • Use          Mage-Test to test...
MAGE-TEST                             http://github.com/ibuildings/Mage-TestWednesday, 9 February 2011
THANK YOU!    • Email: astead@ibuildings.com    • Skype: astead-ibuildings    • Twitter: @alistairsteadWednesday, 9 Februa...
REFERENCES    • PHPUnit                https://github.com/sebastianbergmann/phpunit/    • Zend_Test    http://framework.ze...
IMAGE CREDITS       BA Plane: http://www.flickr.com/photos/bribri/1299325208/sizes/l/in/photostream/       Minions: http://...
QUESTIONS?Wednesday, 9 February 2011
WE ARE HIRING!                             http://www.ibuildings.co.uk/about/careers/Wednesday, 9 February 2011
Upcoming SlideShare
Loading in...5
×

Magento Imagine eCommerce Conference - February 2011 - Unit Testing with Magento

2,493

Published on

Unit Testing with Magento
Alistair Stead, Technical Team Lead,
Session Digital, an Ibuildings Company

0 Comments
4 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
2,493
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
39
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

Magento Imagine eCommerce Conference - February 2011 - Unit Testing with Magento

  1. 1. QUALITY ASSURANCE IN MAGENTO EXTENSIONS Automated Functional TestingWednesday, 9 February 2011
  2. 2. WHO AM I • Alistair Stead • Technical Team Lead @ Ibuildings UK / Session Digital • Lead Magento projects for 3663, WMI, Kookai and others • Over 3 years experience with Magento • Zend Certified Engineer • Over 11 years commercial experience developing in PHPWednesday, 9 February 2011
  3. 3. CREATING SOFTWARE IS COMPLEXWednesday, 9 February 2011
  4. 4. CREATING SOFTWARE IS COMPLEX ing it is argua bly harder) (MaintainWednesday, 9 February 2011
  5. 5. FAULT TOLERANCE IS LOWWednesday, 9 February 2011
  6. 6. “Hello, technical support?” “I would like to report a bug...”Wednesday, 9 February 2011
  7. 7. E-COMMERCE HAS A LOW TOLERANCE TO FAULTS.Wednesday, 9 February 2011
  8. 8. MAGENTO MAKES IT OBVIOUS SOMETHING IS WRONGWednesday, 9 February 2011
  9. 9. With whom should the BUCK stop?Wednesday, 9 February 2011
  10. 10. With whom should the BUCK stop? e sure its not us.... Lets makWednesday, 9 February 2011
  11. 11. SO WHAT CAN WE DO TO FIX THIS?Wednesday, 9 February 2011
  12. 12. We are clever people right?Wednesday, 9 February 2011
  13. 13. Just get more of us?Wednesday, 9 February 2011
  14. 14. Testing at the eleventh hour...Wednesday, 9 February 2011
  15. 15. We can use technology to help!Wednesday, 9 February 2011
  16. 16. AUTOMATED TESTING FRAMEWORKS CAN DO THE HEAVY LIFTING. Selenium WatirWednesday, 9 February 2011
  17. 17. PHPUNIT The de-facto standard for unit testing in PHP projectsWednesday, 9 February 2011
  18. 18. WE CAN RUN UNIT TESTS FOR MAGENTO!Wednesday, 9 February 2011
  19. 19. <?php /** * Magento PHPUnit TestCase * * @package Ibuildings_Mage_Test_PHPUnit * @copyright Copyright (c) 2011 Ibuildings. (http://www.ibuildings.com) * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * @author Alistair Stead <alistair@ibuildings.com> * @version $Id$ */ /** * PHPUnit_Framework_Magento_TestCase * * @category Mage_Test * @package Ibuildings_Mage_Test_PHPUnit * @subpackage Ibuildings_Mage_Test_PHPUnit_TestCase * @uses PHPUnit_Framework_TestCase */ abstract class Ibuildings_Mage_Test_PHPUnit_TestCase extends PHPUnit_Framework_TestCase { public function setUp() { parent::setUp(); // Initialise Magento Mage::app(); } }Wednesday, 9 February 2011
  20. 20. <?php /** * Magento PHPUnit TestCase * * @package Ibuildings_Mage_Test_PHPUnit * @copyright Copyright (c) 2011 Ibuildings. (http://www.ibuildings.com) * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * @author Alistair Stead <alistair@ibuildings.com> * @version $Id$ */ /** * PHPUnit_Framework_Magento_TestCase * * @category Mage_Test * @package Ibuildings_Mage_Test_PHPUnit * @subpackage Ibuildings_Mage_Test_PHPUnit_TestCase * @uses PHPUnit_Framework_TestCase */ abstract class Ibuildings_Mage_Test_PHPUnit_TestCase extends PHPUnit_Framework_TestCase { public function setUp() { parent::setUp(); // Initialise Magento Mage::app(); } }Wednesday, 9 February 2011
  21. 21. MODELSWednesday, 9 February 2011
  22. 22. <?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase { /** * Member variable to hold reference to the opbject under test * * @var DataCash_Dpg_Model_Config **/ protected $_object; /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_object = Mage::getSingleton(dpg/config); }Wednesday, 9 February 2011
  23. 23. <?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase { /** * Member variable to hold reference to the opbject under test * * @var DataCash_Dpg_Model_Config **/ protected $_object; /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_object = Mage::getSingleton(dpg/config); }Wednesday, 9 February 2011
  24. 24. <?php class DataCash_Dpg_Model_ConfigTest extends Ibuildings_Mage_Test_PHPUnit_TestCase { /** * Member variable to hold reference to the opbject under test * * @var DataCash_Dpg_Model_Config **/ protected $_object; /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_object = Mage::getSingleton(dpg/config); }Wednesday, 9 February 2011
  25. 25. /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected object returned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objects have been created); } // mageSingletonFactoryReturnsTheSameObjectWednesday, 9 February 2011
  26. 26. /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected object returned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objects have been created); } // mageSingletonFactoryReturnsTheSameObjectWednesday, 9 February 2011
  27. 27. /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected object returned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objects have been created); } // mageSingletonFactoryReturnsTheSameObjectWednesday, 9 February 2011
  28. 28. /** * getTransactionTypesReturnsArray * @author Alistair Stead * @test */ public function getTransactionTypesReturnsArray() { $result = $this->_object->getTansactionTypes(); $this->assertTrue(is_array($result), No array returned); $this->assertEquals(count($result), 2, More transaction types than expected); $this->assertArrayHasKey( A, $result, The array does not contain A ); $this->assertArrayHasKey( P, $result, The array does not contain P ); } // getTransactionTypesReturnsArrayWednesday, 9 February 2011
  29. 29. /** * getTransactionTypesReturnsArray * @author Alistair Stead * @test */ public function getTransactionTypesReturnsArray() { $result = $this->_object->getTansactionTypes(); $this->assertTrue(is_array($result), No array returned); $this->assertEquals(count($result), 2, More transaction types than expected); $this->assertArrayHasKey( A, $result, The array does not contain A ); $this->assertArrayHasKey( P, $result, The array does not contain P ); } // getTransactionTypesReturnsArrayWednesday, 9 February 2011
  30. 30. HELPERSWednesday, 9 February 2011
  31. 31. /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_helper = Mage::helper(contactsdepartment); $this->_departmentIds = array(); $department = Mage::getModel(contactsdepartment/department); $fixtures = array( array( department_name => department one, department_email => email1@department.com, store_id => 0 ) ); foreach ($fixtures as $data) { $department->addData($data); $department->save(); $this->_departmentIds[] = $department->getId(); } }Wednesday, 9 February 2011
  32. 32. /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_helper = Mage::helper(contactsdepartment); $this->_departmentIds = array(); $department = Mage::getModel(contactsdepartment/department); $fixtures = array( array( department_name => department one, department_email => email1@department.com, store_id => 0 ) ); foreach ($fixtures as $data) { $department->addData($data); $department->save(); $this->_departmentIds[] = $department->getId(); } }Wednesday, 9 February 2011
  33. 33. /** * Setup fixtures and dependencies * * @return void * @author Alistair Stead **/ public function setUp() { parent::setUp(); $this->_helper = Mage::helper(contactsdepartment); $this->_departmentIds = array(); $department = Mage::getModel(contactsdepartment/department); $fixtures = array( array( department_name => department one, department_email => email1@department.com, store_id => 0 ) ); foreach ($fixtures as $data) { $department->addData($data); $department->save(); $this->_departmentIds[] = $department->getId(); } }Wednesday, 9 February 2011
  34. 34. /** * helperFactoryReturnsTheExpectedClass * @author Alistair Stead * @test */ public function helperFactoryReturnsTheExpectedClass() { $this->assertInstanceOf(Ibuildings_ContactsDepartment_Helper_Data, $this->_helper); } // helperFactoryReturnsTheExpectedClass /** * getDepartmentOptionsShouldReturnAssociativeArray * @author Alistair Stead * @test */ public function getDepartmentOptionsShouldReturnAssociativeArray() { foreach ($this->_helper->getDepartmentOptions() as $option) { $this->assertArrayHasKey( value, $option, The options array does not have a value ); $this->assertArrayHasKey( label, $option, The options array does not have a label ); } } // getDepartmentOptionsShouldReturnAssociativeArrayWednesday, 9 February 2011
  35. 35. /** * helperFactoryReturnsTheExpectedClass * @author Alistair Stead * @test */ public function helperFactoryReturnsTheExpectedClass() { $this->assertInstanceOf(Ibuildings_ContactsDepartment_Helper_Data, $this->_helper); } // helperFactoryReturnsTheExpectedClass /** * getDepartmentOptionsShouldReturnAssociativeArray * @author Alistair Stead * @test */ public function getDepartmentOptionsShouldReturnAssociativeArray() { foreach ($this->_helper->getDepartmentOptions() as $option) { $this->assertArrayHasKey( value, $option, The options array does not have a value ); $this->assertArrayHasKey( label, $option, The options array does not have a label ); } } // getDepartmentOptionsShouldReturnAssociativeArrayWednesday, 9 February 2011
  36. 36. /** * helperFactoryReturnsTheExpectedClass * @author Alistair Stead * @test */ public function helperFactoryReturnsTheExpectedClass() { $this->assertInstanceOf(Ibuildings_ContactsDepartment_Helper_Data, $this->_helper); } // helperFactoryReturnsTheExpectedClass /** * getDepartmentOptionsShouldReturnAssociativeArray * @author Alistair Stead * @test */ public function getDepartmentOptionsShouldReturnAssociativeArray() { foreach ($this->_helper->getDepartmentOptions() as $option) { $this->assertArrayHasKey( value, $option, The options array does not have a value ); $this->assertArrayHasKey( label, $option, The options array does not have a label ); } } // getDepartmentOptionsShouldReturnAssociativeArrayWednesday, 9 February 2011
  37. 37. Okay we’re done lets go home...Wednesday, 9 February 2011
  38. 38. Hold on, we have not really done anything new.Wednesday, 9 February 2011
  39. 39. DID WE MENTION FUNCTIONAL TESTING YET?Wednesday, 9 February 2011
  40. 40. QUALITY ASSURANCE IN MAGENTO EXTENSIONS Automated Functional TestingWednesday, 9 February 2011
  41. 41. ZEND_TEST?Wednesday, 9 February 2011
  42. 42. MAGENTO IS CLOSELY COUPLED TO MAGE_CORE_APPLICATIONWednesday, 9 February 2011
  43. 43. Coupled components cause inflexibilityWednesday, 9 February 2011
  44. 44. Dependency injection is the keyWednesday, 9 February 2011
  45. 45. MAGE-TEST http://github.com/ibuildings/Mage-TestWednesday, 9 February 2011
  46. 46. Mage_Core_Model_App Ibuildings_Mage_Controller_Request_HttpTestCase Ibuildings_Mage_Controller_Response_HttpTestCase Ibuildings_Mage_Test_PHPUnit_ControllerTestCase Ibuildings_Mage_Test_PHPUnit_TestCaseWednesday, 9 February 2011
  47. 47. MAGE_CORE_APPLICATION Loaded from the community code poolWednesday, 9 February 2011
  48. 48. /** * Provide a public method to allow the internal Request object * to be set at runtime. This can be used to inject a testing request object * * @return void * @author Alistair Stead **/ public function setRequest(Zend_Controller_Request_Abstract $request) { $this->_request = $request; } /** * Retrieve request object * * @return Mage_Core_Controller_Request_Http */ public function getRequest() { if (empty($this->_request)) { $this->_request = new Mage_Core_Controller_Request_Http(); } return $this->_request; }Wednesday, 9 February 2011
  49. 49. /** * Provide a public method to allow the internal Request object * to be set at runtime. This can be used to inject a testing request object * * @return void * @author Alistair Stead **/ public function setRequest(Zend_Controller_Request_Abstract $request) { $this->_request = $request; } /** * Retrieve request object * * @return Mage_Core_Controller_Request_Http */ public function getRequest() { if (empty($this->_request)) { $this->_request = new Mage_Core_Controller_Request_Http(); } return $this->_request; }Wednesday, 9 February 2011
  50. 50. /** * Provide a public method to allow the protected internal Response object * to be set at runtime. This can be used to inject a testing response object * * @return void * @author Alistair Stead **/ public function setResponse(Zend_Controller_Response_Abstract $response) { $this->_response = $response; } /** * Retrieve response object * * @return Zend_Controller_Response_Http */ public function getResponse() { if (empty($this->_response)) { $this->_response = new Mage_Core_Controller_Response_Http(); $this->_response->headersSentThrowsException = Mage::$headersSentThrowsException; $this->_response->setHeader("Content-Type", "text/html; charset=UTF-8"); } return $this->_response; }Wednesday, 9 February 2011
  51. 51. /** * Provide a public method to allow the protected internal Response object * to be set at runtime. This can be used to inject a testing response object * * @return void * @author Alistair Stead **/ public function setResponse(Zend_Controller_Response_Abstract $response) { $this->_response = $response; } /** * Retrieve response object * * @return Zend_Controller_Response_Http */ public function getResponse() { if (empty($this->_response)) { $this->_response = new Mage_Core_Controller_Response_Http(); $this->_response->headersSentThrowsException = Mage::$headersSentThrowsException; $this->_response->setHeader("Content-Type", "text/html; charset=UTF-8"); } return $this->_response; }Wednesday, 9 February 2011
  52. 52. CONTROLLER TESTS Ibuildings_Mage_Test_PHPUnit_ControllerTestCaseWednesday, 9 February 2011
  53. 53. /** * Bootstrap the Mage application in a similar way to the procedure * of index.php * * Then sets test case request and response objects in Mage_Core_App, * and disables returning the response. * * @return void * @author Alistair Stead */ public function mageBootstrap() { Mage::reset(); if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) { Mage::setIsDeveloperMode(true); } // Store or website code $this->mageRunCode = isset($_SERVER[MAGE_RUN_CODE]) ? $_SERVER[MAGE_RUN_CODE] : ; // Run store or run website $this->mageRunType = isset($_SERVER[MAGE_RUN_TYPE]) ? $_SERVER[MAGE_RUN_TYPE] : store; // Initialize the Mage App and inject the testing request & response Mage::app($this->mageRunCode, $this->mageRunType, $this->options); Mage::app()->setRequest(new Ibuildings_Mage_Controller_Request_HttpTestCase); Mage::app()->setResponse(new Ibuildings_Mage_Controller_Response_HttpTestCase); }Wednesday, 9 February 2011
  54. 54. /** * Bootstrap the Mage application in a similar way to the procedure * of index.php * * Then sets test case request and response objects in Mage_Core_App, * and disables returning the response. * * @return void * @author Alistair Stead */ public function mageBootstrap() { Mage::reset(); if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) { Mage::setIsDeveloperMode(true); } // Store or website code $this->mageRunCode = isset($_SERVER[MAGE_RUN_CODE]) ? $_SERVER[MAGE_RUN_CODE] : ; // Run store or run website $this->mageRunType = isset($_SERVER[MAGE_RUN_TYPE]) ? $_SERVER[MAGE_RUN_TYPE] : store; // Initialize the Mage App and inject the testing request & response Mage::app($this->mageRunCode, $this->mageRunType, $this->options); Mage::app()->setRequest(new Ibuildings_Mage_Controller_Request_HttpTestCase); Mage::app()->setResponse(new Ibuildings_Mage_Controller_Response_HttpTestCase); }Wednesday, 9 February 2011
  55. 55. /** * Bootstrap the Mage application in a similar way to the procedure * of index.php * * Then sets test case request and response objects in Mage_Core_App, * and disables returning the response. * * @return void * @author Alistair Stead */ public function mageBootstrap() { Mage::reset(); if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) { Mage::setIsDeveloperMode(true); } // Store or website code $this->mageRunCode = isset($_SERVER[MAGE_RUN_CODE]) ? $_SERVER[MAGE_RUN_CODE] : ; // Run store or run website $this->mageRunType = isset($_SERVER[MAGE_RUN_TYPE]) ? $_SERVER[MAGE_RUN_TYPE] : store; // Initialize the Mage App and inject the testing request & response Mage::app($this->mageRunCode, $this->mageRunType, $this->options); Mage::app()->setRequest(new Ibuildings_Mage_Controller_Request_HttpTestCase); Mage::app()->setResponse(new Ibuildings_Mage_Controller_Response_HttpTestCase); }Wednesday, 9 February 2011
  56. 56. Ibuildings_Mage_Controller_Request_HttpTestCase Model View Controller Ibuildings_Mage_Controller_Response_HttpTestCaseWednesday, 9 February 2011
  57. 57. Ibuildings_Mage_Controller_Request_HttpTestCase Model View Controller Ibuildings_Mage_Controller_Response_HttpTestCaseWednesday, 9 February 2011
  58. 58. Mage_Core_Model_App Ibuildings_Mage_Controller_Request_HttpTestCase Model View Controller Ibuildings_Mage_Controller_Response_HttpTestCaseWednesday, 9 February 2011
  59. 59. <?php /** * Mage_Catalog_IndexControllerTest * * @package Mage_Catalog * @subpackage Mage_Catalog_Test * * * @uses PHPUnit_Framework_Magento_TestCase */ class Mage_Catalog_IndexControllerTest extends Ibuildings_Mage_Test_PHPUnit_ControllerTestCase { /** * theIndexActionShouldRedirectToRoot * @author Alistair Stead * @test */ public function theIndexActionShouldRedirectToRoot() { $this->dispatch(/); $this->assertRoute(cms, "The expected cms route has not been matched"); $this->assertAction(index, "The index action has not been called"); $this->assertController(index, "The expected controller is not been used"); $this->assertQuery(div.nav-container, The site navigation is not present on the home page); } // theIndexActionShouldRedirectToRoot }Wednesday, 9 February 2011
  60. 60. <?php /** * Mage_Catalog_IndexControllerTest * * @package Mage_Catalog * @subpackage Mage_Catalog_Test * * * @uses PHPUnit_Framework_Magento_TestCase */ class Mage_Catalog_IndexControllerTest extends Ibuildings_Mage_Test_PHPUnit_ControllerTestCase { /** * theIndexActionShouldRedirectToRoot * @author Alistair Stead * @test */ public function theIndexActionShouldRedirectToRoot() { $this->dispatch(/); $this->assertRoute(cms, "The expected cms route has not been matched"); $this->assertAction(index, "The index action has not been called"); $this->assertController(index, "The expected controller is not been used"); $this->assertQuery(div.nav-container, The site navigation is not present on the home page); } // theIndexActionShouldRedirectToRoot }Wednesday, 9 February 2011
  61. 61. <?php /** * Mage_Catalog_IndexControllerTest * * @package Mage_Catalog * @subpackage Mage_Catalog_Test * * * @uses PHPUnit_Framework_Magento_TestCase */ class Mage_Catalog_IndexControllerTest extends Ibuildings_Mage_Test_PHPUnit_ControllerTestCase { /** * theIndexActionShouldRedirectToRoot * @author Alistair Stead * @test */ public function theIndexActionShouldRedirectToRoot() { $this->dispatch(/); $this->assertRoute(cms, "The expected cms route has not been matched"); $this->assertAction(index, "The index action has not been called"); $this->assertController(index, "The expected controller is not been used"); $this->assertQuery(div.nav-container, The site navigation is not present on the home page); } // theIndexActionShouldRedirectToRoot }Wednesday, 9 February 2011
  62. 62. /** * theAdminRouteAccessesTheAdminApplicationArea * @author Alistair Stead * @test */ public function theAdminRouteAccessesTheAdminApplicationArea() { $this->dispatch(admin/); $this->assertRoute(adminhtml, "The expected route has not been matched"); $this->assertAction(login, "The login form should be presented"); $this->assertController(index, "The expected controller is not been used"); } // theAdminRouteAccessesTheAdminApplicationArea /** * theIndexActionDisplaysLoginForm * @author Alistair Stead * @group login * @test */ public function theIndexActionDisplaysLoginForm() { $this->dispatch(admin/index/); $this->assertQueryCount(form#loginForm, 1); } // theIndexActionDisplaysLoginFormWednesday, 9 February 2011
  63. 63. /** * theAdminRouteAccessesTheAdminApplicationArea * @author Alistair Stead * @test */ public function theAdminRouteAccessesTheAdminApplicationArea() { $this->dispatch(admin/); $this->assertRoute(adminhtml, "The expected route has not been matched"); $this->assertAction(login, "The login form should be presented"); $this->assertController(index, "The expected controller is not been used"); } // theAdminRouteAccessesTheAdminApplicationArea /** * theIndexActionDisplaysLoginForm * @author Alistair Stead * @group login * @test */ public function theIndexActionDisplaysLoginForm() { $this->dispatch(admin/index/); $this->assertQueryCount(form#loginForm, 1); } // theIndexActionDisplaysLoginFormWednesday, 9 February 2011
  64. 64. /** * theAdminRouteAccessesTheAdminApplicationArea * @author Alistair Stead * @test */ public function theAdminRouteAccessesTheAdminApplicationArea() { $this->dispatch(admin/); $this->assertRoute(adminhtml, "The expected route has not been matched"); $this->assertAction(login, "The login form should be presented"); $this->assertController(index, "The expected controller is not been used"); } // theAdminRouteAccessesTheAdminApplicationArea /** * theIndexActionDisplaysLoginForm * @author Alistair Stead * @group login * @test */ public function theIndexActionDisplaysLoginForm() { $this->dispatch(admin/index/); $this->assertQueryCount(form#loginForm, 1); } // theIndexActionDisplaysLoginFormWednesday, 9 February 2011
  65. 65. /** * theAdminRouteAccessesTheAdminApplicationArea * @author Alistair Stead * @test */ public function theAdminRouteAccessesTheAdminApplicationArea() { $this->dispatch(admin/); $this->assertRoute(adminhtml, "The expected route has not been matched"); $this->assertAction(login, "The login form should be presented"); $this->assertController(index, "The expected controller is not been used"); } // theAdminRouteAccessesTheAdminApplicationArea /** * theIndexActionDisplaysLoginForm * @author Alistair Stead * @group login * @test */ public function theIndexActionDisplaysLoginForm() { $this->dispatch(admin/index/); $this->assertQueryCount(form#loginForm, 1); } // theIndexActionDisplaysLoginFormWednesday, 9 February 2011
  66. 66. /** * theAdminRouteAccessesTheAdminApplicationArea * @author Alistair Stead * @test */ public function theAdminRouteAccessesTheAdminApplicationArea() { $this->dispatch(admin/); $this->assertRoute(adminhtml, "The expected route has not been matched"); $this->assertAction(login, "The login form should be presented"); $this->assertController(index, "The expected controller is not been used"); } // theAdminRouteAccessesTheAdminApplicationArea /** * theIndexActionDisplaysLoginForm * @author Alistair Stead * @group login * @test */ public function theIndexActionDisplaysLoginForm() { $this->dispatch(admin/index/); $this->assertQueryCount(form#loginForm, 1); } // theIndexActionDisplaysLoginFormWednesday, 9 February 2011
  67. 67. /** * submittingInvalidCredsShouldDisplayError * @author Alistair Stead * @group login * @test */ public function submittingInvalidCredsShouldDisplayError() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => invalid-password, ) ) ); $this->dispatch(admin/index/login); $this->assertQueryCount(li.error-msg, 1); $this->assertQueryContentContains(li.error-msg, Invalid Username or Password.); } // submittingInvalidCredsShouldDisplayErrorWednesday, 9 February 2011
  68. 68. /** * submittingInvalidCredsShouldDisplayError * @author Alistair Stead * @group login * @test */ public function submittingInvalidCredsShouldDisplayError() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => invalid-password, ) ) ); $this->dispatch(admin/index/login); $this->assertQueryCount(li.error-msg, 1); $this->assertQueryContentContains(li.error-msg, Invalid Username or Password.); } // submittingInvalidCredsShouldDisplayErrorWednesday, 9 February 2011
  69. 69. /** * submittingInvalidCredsShouldDisplayError * @author Alistair Stead * @group login * @test */ public function submittingInvalidCredsShouldDisplayError() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => invalid-password, ) ) ); $this->dispatch(admin/index/login); $this->assertQueryCount(li.error-msg, 1); $this->assertQueryContentContains(li.error-msg, Invalid Username or Password.); } // submittingInvalidCredsShouldDisplayErrorWednesday, 9 February 2011
  70. 70. /** * submittingInvalidCredsShouldDisplayError * @author Alistair Stead * @group login * @test */ public function submittingInvalidCredsShouldDisplayError() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => invalid-password, ) ) ); $this->dispatch(admin/index/login); $this->assertQueryCount(li.error-msg, 1); $this->assertQueryContentContains(li.error-msg, Invalid Username or Password.); } // submittingInvalidCredsShouldDisplayErrorWednesday, 9 February 2011
  71. 71. /** * submittingInvalidCredsShouldDisplayError * @author Alistair Stead * @group login * @test */ public function submittingInvalidCredsShouldDisplayError() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => invalid-password, ) ) ); $this->dispatch(admin/index/login); $this->assertQueryCount(li.error-msg, 1); $this->assertQueryContentContains(li.error-msg, Invalid Username or Password.); } // submittingInvalidCredsShouldDisplayErrorWednesday, 9 February 2011
  72. 72. /** * submittingValidCredsShouldDisplayDashboard * @author Alistair Stead * @group login * @test */ public function submittingValidCredsShouldDisplayDashboard() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => 123456, ) ) ); $this->dispatch(admin/index/login); $this->assertRedirect(We should be redirected after login); $this->assertRedirectRegex("/^.*dashboard.*$/", We are not directed to the dashboard); } // submittingValidCredsShouldDisplayDashboardWednesday, 9 February 2011
  73. 73. /** * submittingValidCredsShouldDisplayDashboard * @author Alistair Stead * @group login * @test */ public function submittingValidCredsShouldDisplayDashboard() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => 123456, ) ) ); $this->dispatch(admin/index/login); $this->assertRedirect(We should be redirected after login); $this->assertRedirectRegex("/^.*dashboard.*$/", We are not directed to the dashboard); } // submittingValidCredsShouldDisplayDashboardWednesday, 9 February 2011
  74. 74. class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase { /** * indexActionListsOrders * @author Alistair Stead * @test */ public function indexActionListsOrders() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => 123456, ) ) ); $this->dispatch(admin/index/login); // Reset the requests after login before next dispatch $this->reset(); $this->dispatch(admin/sales_order/index); $this->assertQueryContentContains(h3.icon-head, Orders); } // indexActionListsOrders}Wednesday, 9 February 2011
  75. 75. class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase { /** * indexActionListsOrders * @author Alistair Stead * @test */ public function indexActionListsOrders() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => 123456, ) ) ); $this->dispatch(admin/index/login); // Reset the requests after login before next dispatch $this->reset(); $this->dispatch(admin/sales_order/index); $this->assertQueryContentContains(h3.icon-head, Orders); } // indexActionListsOrders}Wednesday, 9 February 2011
  76. 76. class Mage_Adminhtml_SalesControllerTest extends Mage_Adminhtml_ControllerTestCase { /** * indexActionListsOrders * @author Alistair Stead * @test */ public function indexActionListsOrders() { $this->request->setMethod(POST) ->setPost( array( login => array( username => admin, password => 123456, ) ) ); $this->dispatch(admin/index/login); // Reset the requests after login before next dispatch $this->reset(); $this->dispatch(admin/sales_order/index); $this->assertQueryContentContains(h3.icon-head, Orders); } // indexActionListsOrders}Wednesday, 9 February 2011
  77. 77. TESTING EMAIL CONTENT Who gets all the emails sent to test@example.com?Wednesday, 9 February 2011
  78. 78. public function mageBootstrap() { Mage::reset(); if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) { Mage::setIsDeveloperMode(true); } // Store or website code $this->mageRunCode = isset($_SERVER[MAGE_RUN_CODE]) ? $_SERVER[MAGE_RUN_CODE] : ; // Run store or run website $this->mageRunType = isset($_SERVER[MAGE_RUN_TYPE]) ? $_SERVER[MAGE_RUN_TYPE] : store; // Initialize the Mage App and inject the testing request & response Mage::app($this->mageRunCode, $this->mageRunType, $this->options); Mage::app()->setRequest(new Ibuildings_Mage_Controller_Request_HttpTestCase); Mage::app()->setResponse(new Ibuildings_Mage_Controller_Response_HttpTestCase); // Rewrite the core classes at runtime to prevent emails from being sent Mage::getConfig()->setNode(global/models/core/rewrite/email_template,Ibuildings_Test_Model_Email_Template); // This is a hack to get the runtime config changes to take effect Mage::getModel(core/email_template); }Wednesday, 9 February 2011
  79. 79. public function mageBootstrap() { Mage::reset(); if (isset($_SERVER[MAGE_IS_DEVELOPER_MODE])) { Mage::setIsDeveloperMode(true); } // Store or website code $this->mageRunCode = isset($_SERVER[MAGE_RUN_CODE]) ? $_SERVER[MAGE_RUN_CODE] : ; // Run store or run website $this->mageRunType = isset($_SERVER[MAGE_RUN_TYPE]) ? $_SERVER[MAGE_RUN_TYPE] : store; // Initialize the Mage App and inject the testing request & response Mage::app($this->mageRunCode, $this->mageRunType, $this->options); Mage::app()->setRequest(new Ibuildings_Mage_Controller_Request_HttpTestCase); Mage::app()->setResponse(new Ibuildings_Mage_Controller_Response_HttpTestCase); // Rewrite the core classes at runtime to prevent emails from being sent Mage::getConfig()->setNode(global/models/core/rewrite/email_template,Ibuildings_Test_Model_Email_Template); // This is a hack to get the runtime config changes to take effect Mage::getModel(core/email_template); }Wednesday, 9 February 2011
  80. 80. /** * submittingForgotPasswordWithValidEmailReturnsSuccess * @author Alistair Stead * @group password * @test * */ public function submittingForgotPasswordWithValidEmailReturnsSuccess() { $this->request->setMethod(POST) ->setPost(array(email => $this->email)); $this->dispatch(admin/index/forgotpassword/); $this->assertQueryCount(li.success-msg, 1); $this->assertQueryContentContains(li.success-msg, A new password was sent to your emailaddress. Please check your email and click Back to Login.); // Test that the email contains the correct data $emailContent = $this->getResponseEmail() ->getBodyHtml() ->getContent(); // Overriding the response body to be able to use the standard content assertions $this->response->setBody($emailContent); // The email content addresses the fixture user $this->assertQueryContentContains(body, "Dear $this->firstName $this->lastName"); // The fixture users password has been changed $this->assertNotQueryContentContains(body, $this->password); } // submittingForgotPasswordWithValidEmailReturnsSuccessWednesday, 9 February 2011
  81. 81. /** * submittingForgotPasswordWithValidEmailReturnsSuccess * @author Alistair Stead * @group password * @test * */ public function submittingForgotPasswordWithValidEmailReturnsSuccess() { $this->request->setMethod(POST) ->setPost(array(email => $this->email)); $this->dispatch(admin/index/forgotpassword/); $this->assertQueryCount(li.success-msg, 1); $this->assertQueryContentContains(li.success-msg, A new password was sent to your emailaddress. Please check your email and click Back to Login.); // Test that the email contains the correct data $emailContent = $this->getResponseEmail() ->getBodyHtml() ->getContent(); // Overriding the response body to be able to use the standard content assertions $this->response->setBody($emailContent); // The email content addresses the fixture user $this->assertQueryContentContains(body, "Dear $this->firstName $this->lastName"); // The fixture users password has been changed $this->assertNotQueryContentContains(body, $this->password); } // submittingForgotPasswordWithValidEmailReturnsSuccessWednesday, 9 February 2011
  82. 82. /** * submittingForgotPasswordWithValidEmailReturnsSuccess * @author Alistair Stead * @group password * @test * */ public function submittingForgotPasswordWithValidEmailReturnsSuccess() { $this->request->setMethod(POST) ->setPost(array(email => $this->email)); $this->dispatch(admin/index/forgotpassword/); $this->assertQueryCount(li.success-msg, 1); $this->assertQueryContentContains(li.success-msg, A new password was sent to your emailaddress. Please check your email and click Back to Login.); // Test that the email contains the correct data $emailContent = $this->getResponseEmail() ->getBodyHtml() ->getContent(); // Overriding the response body to be able to use the standard content assertions $this->response->setBody($emailContent); // The email content addresses the fixture user $this->assertQueryContentContains(body, "Dear $this->firstName $this->lastName"); // The fixture users password has been changed $this->assertNotQueryContentContains(body, $this->password); } // submittingForgotPasswordWithValidEmailReturnsSuccessWednesday, 9 February 2011
  83. 83. /** * submittingForgotPasswordWithValidEmailReturnsSuccess * @author Alistair Stead * @group password * @test * */ public function submittingForgotPasswordWithValidEmailReturnsSuccess() { $this->request->setMethod(POST) ->setPost(array(email => $this->email)); $this->dispatch(admin/index/forgotpassword/); $this->assertQueryCount(li.success-msg, 1); $this->assertQueryContentContains(li.success-msg, A new password was sent to your emailaddress. Please check your email and click Back to Login.); // Test that the email contains the correct data $emailContent = $this->getResponseEmail() ->getBodyHtml() ->getContent(); // Overriding the response body to be able to use the standard content assertions $this->response->setBody($emailContent); // The email content addresses the fixture user $this->assertQueryContentContains(body, "Dear $this->firstName $this->lastName"); // The fixture users password has been changed $this->assertNotQueryContentContains(body, $this->password); } // submittingForgotPasswordWithValidEmailReturnsSuccessWednesday, 9 February 2011
  84. 84. SO WHAT DO AUTOMATED TESTS GIVE US?Wednesday, 9 February 2011
  85. 85. CODE COVERAGEWednesday, 9 February 2011
  86. 86. STATIC CODE ANALYSISWednesday, 9 February 2011
  87. 87. CONTINUOUS INTEGRATIONWednesday, 9 February 2011
  88. 88. Wednesday, 9 February 2011
  89. 89. Wednesday, 9 February 2011
  90. 90. SO WHAT NOW?Wednesday, 9 February 2011
  91. 91. MAGE-TEST IS OPEN SOURCE • Use Mage-Test to test your development projects • Use Mage-Test to test you extensions • Contribute tests for Magento • Build up the coverage of the Magento codebase • Take advantage of core tests for regression testingWednesday, 9 February 2011
  92. 92. MAGE-TEST http://github.com/ibuildings/Mage-TestWednesday, 9 February 2011
  93. 93. THANK YOU! • Email: astead@ibuildings.com • Skype: astead-ibuildings • Twitter: @alistairsteadWednesday, 9 February 2011
  94. 94. REFERENCES • PHPUnit https://github.com/sebastianbergmann/phpunit/ • Zend_Test http://framework.zend.com/manual/en/ zend.test.html • Mage-Test https://github.com/ibuildings/Mage-Test • phpUnderControl http://phpundercontrol.org/ • Bamboo http://www.atlassian.com/software/bamboo/Wednesday, 9 February 2011
  95. 95. IMAGE CREDITS BA Plane: http://www.flickr.com/photos/bribri/1299325208/sizes/l/in/photostream/ Minions: http://www.akblessingsabound.com/wp-content/uploads/2010/06/despicable-me-minions- blessings-abound-mommy.jpg Eleventh Hour: http://www.flickr.com/photos/d4dee/2258343575/sizes/l/in/photostream/ Deep Thought: http://www.flickr.com/photos/8640416@N02/4213361072/sizes/l/in/photostream/ US Dollar: http://www.flickr.com/photos/8640416@N02/4213361072/sizes/l/in/photostream/ Whoa (Stop Sign): http://www.flickr.com/photos/aquaoracle/3265987824/sizes/l/ Departures Board: http://www.flickr.com/photos/scottmulhollan/4892422469/sizes/l/in/photostream/ Syringe: http://www.flickr.com/photos/woodypics/3809842998/sizes/z/in/photostream/ Coupled Carriages: http://www.flickr.com/photos/jowo/89657494/sizes/o/in/photostream/Wednesday, 9 February 2011
  96. 96. QUESTIONS?Wednesday, 9 February 2011
  97. 97. WE ARE HIRING! http://www.ibuildings.co.uk/about/careers/Wednesday, 9 February 2011
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×