Quality Assurance for Magento
Upcoming SlideShare
Loading in...5
×
 

Quality Assurance for Magento

on

  • 6,196 views

In this technical session we will look at how to add unit and functional testing to your Magento development processes. Using standard testing tools such as PHPUnit, Selenium and Hudson we will add ...

In this technical session we will look at how to add unit and functional testing to your Magento development processes. Using standard testing tools such as PHPUnit, Selenium and Hudson we will add measurable quality assurance practices to Magento development. Based on real world experience with some of the largest Magento sites in Europe, we will look at the challenges faced by development teams trying to test their projects. We will present examples of how Mage_Test can help overcome these challenges and increase the confidence and stability of any development. We will see how the tests can be used within a continuous integration system such as Hudson, presenting reports so that project stake holders can feel confident ahead of a project release.

Statistics

Views

Total Views
6,196
Views on SlideShare
6,194
Embed Views
2

Actions

Likes
4
Downloads
107
Comments
0

1 Embed 2

https://twitter.com 2

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • Hi... welcome....I will be talking about automated functional testing within Magento.\nWe all test our application to some extent; don’t we? \nEither manually of with the use of testing tools tools.\nToday I will be focussing on the use of PHPUnit during development to increase code stability and quality assurance ahead of deployment.\n
  • Hi, I’m Alistair. I’m Technical Team Lead @ \nI have a keen interest in development processes and practices and\nusing additional technology or processes from other languages to support PHP development.\n
  • Creating software is hard..\nAdding new features is the easy and fun part..\n.....\nMaintaining and refactoring software without breaking existing functionality is harder! and sometimes not so much fun.\nWe need development practices and tools to help make this easier.\n
  • There are key applications of technology and software, where fault tolerance is very low.\n\n
  • Software applications are key components in many processes. \nWe increasingly rely on software in many aspects of life.\nMajor bugs simply can not be tolerated.\n\nBrochure websites are fairly low risk they can be updated quickly with little impact? \nWeb application however have more risk, users will be unable to use your application if it is broken...\n\n
  • E-commerce is an application of technology where fault tolerance is very low.\nVendors are acutely tuned to the income associated with their store.\nIf it dramatically falls something must be wrong! right?\n
  • It is painfully easy for vendors to measure losses as a result of errors in their store.\nWhen your client logs into the admin screen and sales have completely stopped. \nWho will they call?\n
  • If something is broken who is going to fix it? We are...\nIf you have just deployed some new functionality, do you know where this bug has been introduced?\nHow long will it take to fix?\nHow can you make sure this same bug does not happen some time in the future.\n.....\nLets make sure the buck does not stop with us....\n\n
  • How can we ensure that the software we write is functioning correctly now and in the future?\nHow can we reduce the risks of adding new features and re-factoring?\nHow can we reduce the risks of changing functionality?\n\n\n
  • We study long and hard to be good at what we do.\nBut that does not change that facts, software engineering is a complex task.\n* We are busy people we miss things\n* We only test isolated things...\nIf we find and bugs during development we fix them, thats not really questioned.\nHowever how do we make sure they stay fixed?\n
  • We could draft in lots of people to test the application. \nAfter all many hands make light work?\nHowever many people cost much more money.\nSo how often will we do the testing with this many people?\n\nAfter every small code change......?\n
  • ....or at the very end when your trying to meet a deadline?\nTesting this late will lead to missed deadlines and frustrated clients\n\nWhen will you have time to fix the bugs?\n
  • Use technology for what it is good at.\nRepeating steps the same way time after time.\nValidating the results are the same for the same inputs.\n\nWhat practices can we introduce?\nWhat tools can we use to help?\n
  • Automated testing frameworks can do the heavy lifting.\nTools such as Selenium or Watir, there are many others. \nSelenium & Watir require the application to be run through a web server and to have additional systems running.\nThese can be good for final testing and can offer full browser and JavaScript testing but they do not offer the immediate feedback that can allow developers to test the application as they build it.\n\nPHP has testing frameworks of its own. The most common of which is PHPUnit\n
  • PHPUnit is the de-facto standard for unit testing in PHP projects. \nIt provides both a framework that makes the writing of tests easy as well as the functionality to easily run the tests and analyse their results.\n
  • Straight off and with little effort we can run Unit Tests for Magento with PHPUnit.\nWe can test isolated units of code from our models and helpers an confirm that they are functioning correctly.\nThere are indeed examples already on the Magento WIKI.\n
  • Within a class that extends PHPUnit_Framework_TestCase.\n.....\nAll we need to do is initialise Magento in our setUp method of our test case.\nThis is enough to have setup the Magento autoloader and initialise Magento.\nThis gives us access to objects through the Mage.php static factory methods.\n
  • We can then test a model in isolation\n
  • First create a test class that extends our test case\n.....\nThen within our setUp method we first call parent::setUp() to initialise Magento\nAlso within out setup method we can create testing fixtures or setup and dependencies for our tests.\n\nIn this case we have created a new model object using the Mage::getSingleton method.\n\n
  • First create a test class that extends our test case\n.....\nThen within our setUp method we first call parent::setUp() to initialise Magento\nAlso within out setup method we can create testing fixtures or setup and dependencies for our tests.\n\nIn this case we have created a new model object using the Mage::getSingleton method.\n\n
  • After we have setup our class we can create test methods that will test different aspects of the model and make assertions about the results.\n....\nIn this example we first test that the static factory has returned an object of the correct type. If we are overloading Magento objects this can allow you to confirm that your config is changing the results of the factory methods.\n....\nIn the second test we are confirming that the object is being maintained as a singleton.\n
  • After we have setup our class we can create test methods that will test different aspects of the model and make assertions about the results.\n....\nIn this example we first test that the static factory has returned an object of the correct type. If we are overloading Magento objects this can allow you to confirm that your config is changing the results of the factory methods.\n....\nIn the second test we are confirming that the object is being maintained as a singleton.\n
  • We can also call methods of the object and make assertions against the return values.\n....\nIn this example we are using the standard PHPUnit assertions to confirm that our model getTransactionTypes method returns and associative array, with the expected number items and the expected keys.\n
  • We can also test helpers in isolation\n
  • In this class the setup method is very similar.\n.....\nWe first call parent::setUp() then we create a new helper and assign it to an internal property variable.\n.....\nBecause this helper will retrieve information from the data layer we first need to insert some fixtures that can be used for testing and then removed during tearDown()\n
  • In this class the setup method is very similar.\n.....\nWe first call parent::setUp() then we create a new helper and assign it to an internal property variable.\n.....\nBecause this helper will retrieve information from the data layer we first need to insert some fixtures that can be used for testing and then removed during tearDown()\n
  • We can then start to add test methods for our helper.\n.....\nIn the first test we confirm that the helper factory has returned the correct object.\n.....\nThen in the second test we confirm that that return value of the helper method is what we expect.\n
  • We can then start to add test methods for our helper.\n.....\nIn the first test we confirm that the helper factory has returned the correct object.\n.....\nThen in the second test we confirm that that return value of the helper method is what we expect.\n
  • Okay so that was unit testing with Magento. We can now write tests classes that we can run to validate that our models and helpers function correctly.\n\nThat didn’t take long.\n
  • Hold on we have not covered anything particularly new yet.\nPHPUnit and unit testing model classes is not ground breaking.\n
  • Have I mentioned anything to do with functional testing yet?\n
  • After all it was what we set out to cover....\n
  • Okay what about Zend_Test?\nZend framework application affords this functionality using Zend_Test\nZend_Test although shipped with Magento is not compatible.\nZend_Test was designed to function with Zend_Controller / Zend_Application\nMagento was designed before Zend_Controller\n
  • Magento is closely coupled to the Mage_Core_Application during bootstrap.\nWhich is then coupled to Mage_Core_Controller_Request_Http and Mage_Core_Controller_Response_Http\n
  • These closely coupled components present us with a problem trying to provide functional testing tools for Magento.\n\n
  • Dependency injection is the key.\nIt provides flexibility through composition.\nIn most areas of Magento this is handled by the configurable static factory methods of Mage.php but during initial bootstrap of Magento there are some objects instantiated without using the factory.\n
  • Enter Mage-Test as a solution.\nMage-Test amongst other things provides a patched version of Mage_Core_Application\n\nThis patched app class allows us to inject the dependencies of Request and Response objects are run time. \nThis means we can replace the core objects with testing objects when we bootstrap Magento.\n
  • Mage-Test consists primarily 5 key classes.\nMage_Core_Model_App a patched version of Magento app.\nRequest_HttpTestCase and Response_HttpTestCase fake objects that are injected during bootstrap.\nIbuildings_Mage_Test_PHPUnit_ControllerTestCase a test case used for testing the controllers.\nIbuildings_Mage_Test_PHPUnit_TestCase a test case for testing models and helpers.\nThere are other minor patches applied but these are not making changes to functionality.\n
  • This patched version is loaded by the prioritised autoloader loading the file from the community code pool when the object is instantiated in Mage.php\n\n
  • Within the new version of Mage_Core_Application we have added\n.....\na setter method for the request object.\nThe getRequest method remains the same and because the internal $_request property is pre-set during bootstrap and the referenced object internal is used.\n
  • We have also added\n.....\na setter method for the response object.\nThe getResponse method remains the same and because the internal $_response property is pre-set during bootstrap and again the referenced internal object is used.\n
  • Controller test case provides the same functionality as Zend_Test ControllerTest.\nIt provides all the assertions available in Zend_Test, ported to be compatible with Magento.\nIt is within the ControllerTestCase that we will bootstrap Magento and dispatch requests to confirm that the magento application stack functions correctly.\n
  • During the bootstrap of Magento the standard \n.....\n$_SERVER variables allow you to configure Magento using the same mechanisms but instead you would set the $_SERVER variables manually.\n.....\nThen the testing request and response objects are injected.\nImportantly we do not call Mage::run() as this would execute a full request response cycle within Magento before we have configured a fake request object.\n
  • During the bootstrap of Magento the standard \n.....\n$_SERVER variables allow you to configure Magento using the same mechanisms but instead you would set the $_SERVER variables manually.\n.....\nThen the testing request and response objects are injected.\nImportantly we do not call Mage::run() as this would execute a full request response cycle within Magento before we have configured a fake request object.\n
  • Rather than the request being created by the browser\n.....\nWe build configure our fake request object and dispatch the request through the Magento application controller directly.\nThe request is not transmitted via HTTP to an Apache instance is is sent straight to the Magento application stack.\nThe Controller marshalling the transfer or request parameters to the view and model layers with no difference to the request being generated by a users browser.\n.....\nThe fake Response is built in exactly the same way as a standard response with However the response object does not stream the HTTP headers and response body back to a users browser.\nInstead it stores and maintains the values internally to allow assertions to be made against them later.\n
  • Rather than the request being created by the browser\n.....\nWe build configure our fake request object and dispatch the request through the Magento application controller directly.\nThe request is not transmitted via HTTP to an Apache instance is is sent straight to the Magento application stack.\nThe Controller marshalling the transfer or request parameters to the view and model layers with no difference to the request being generated by a users browser.\n.....\nThe fake Response is built in exactly the same way as a standard response with However the response object does not stream the HTTP headers and response body back to a users browser.\nInstead it stores and maintains the values internally to allow assertions to be made against them later.\n
  • In order to run controller tests we first need to extend the ControllerTestCase\nWe can still extend the setUp method to add fixtures that may be required for testing the controller.\n....\nWhen we dispatch the request, in this example to the root URL the complete application request response cycle is executed. This will build a complete fake response object including the HTML that would normally be presented to the user in their browser.\n.....\nWe can then make assertions against the content of the response.\nWe can validate that:\n* the correct route was matched\n* the correct action was execute\n* the correct controller was used\n* the HTML body has been constructed correctly\n\nThe assertions use CSS3 paths to identify the nodes within the HTML body of the response.\n
  • In order to run controller tests we first need to extend the ControllerTestCase\nWe can still extend the setUp method to add fixtures that may be required for testing the controller.\n....\nWhen we dispatch the request, in this example to the root URL the complete application request response cycle is executed. This will build a complete fake response object including the HTML that would normally be presented to the user in their browser.\n.....\nWe can then make assertions against the content of the response.\nWe can validate that:\n* the correct route was matched\n* the correct action was execute\n* the correct controller was used\n* the HTML body has been constructed correctly\n\nThe assertions use CSS3 paths to identify the nodes within the HTML body of the response.\n
  • In this example test class that tests the admin functionality.\n.....\nWe first dispatch a request to the admin URL\n.....\nThen we make assertions about the route, action and controller to ensure that Magento is correctly responding to the request for the admin path.\n.....\nIn the second test we we call the admin module index controller directly.\n.....\nThen we assert that the response body includes the HTML form for the admin login.\n
  • In this example test class that tests the admin functionality.\n.....\nWe first dispatch a request to the admin URL\n.....\nThen we make assertions about the route, action and controller to ensure that Magento is correctly responding to the request for the admin path.\n.....\nIn the second test we we call the admin module index controller directly.\n.....\nThen we assert that the response body includes the HTML form for the admin login.\n
  • In this example test class that tests the admin functionality.\n.....\nWe first dispatch a request to the admin URL\n.....\nThen we make assertions about the route, action and controller to ensure that Magento is correctly responding to the request for the admin path.\n.....\nIn the second test we we call the admin module index controller directly.\n.....\nThen we assert that the response body includes the HTML form for the admin login.\n
  • In this example test class that tests the admin functionality.\n.....\nWe first dispatch a request to the admin URL\n.....\nThen we make assertions about the route, action and controller to ensure that Magento is correctly responding to the request for the admin path.\n.....\nIn the second test we we call the admin module index controller directly.\n.....\nThen we assert that the response body includes the HTML form for the admin login.\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • During automated testing we do not want email to be sent.\nHowever we do need to ensure any email includes the content we expect.\nCan we automate the testing of email also?\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Helping us measure the risk of adding new features\n
  • Measure the complexity of the code.\nHelping us monitor the code quality created by a team.\nHelping us enforce coding standards.\n
  • Monitoring for introduction of poor quality code.\nMonitoring the introduction of defects.\nRunning regression tests to ensure previously fixed bugs have not been re-introduced.\n
  • \n
  • \n
  • So what now....\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n

Quality Assurance for Magento Quality Assurance for Magento Presentation Transcript

  • QUALITY ASSURANCE INMAGENTO EXTENSIONS Automated Functional Testing
  • 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 PHP
  • CREATING SOFTWARE IS COMPLEX
  • CREATING SOFTWARE IS COMPLEX ning it is argu ably harder) (Maintai
  • FAULT TOLERANCE IS LOW
  • “Hello, technical support?” “I would like to report a bug...”
  • E-COMMERCE HAS A LOW TOLERANCE TO FAULTS.
  • MAGENTO MAKES ITOBVIOUS SOMETHING IS WRONG
  • With whom should the BUCK stop?
  • With whom should the BUCK stop? ke sure its not us....Lets ma
  • SO WHAT CAN WE DO TO FIX THIS?
  • We are clever people right?
  • Just get more of us?
  • Testing at the eleventh hour...
  • We can usetechnology to help!
  • AUTOMATED TESTINGFRAMEWORKS CAN DO THE HEAVY LIFTING. Selenium Watir
  • PHPUNITThe de-facto standard for unit testing in PHP projects
  • WE CAN RUN UNIT TESTS FOR MAGENTO!
  • <?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(); }}
  • <?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(); }}
  • MODELS
  • <?phpclass 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); }
  • <?phpclass 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); }
  • <?phpclass 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); }
  • /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected objectreturned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objectshave been created); } // mageSingletonFactoryReturnsTheSameObject
  • /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected objectreturned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objectshave been created); } // mageSingletonFactoryReturnsTheSameObject
  • /** * mageSingletonFactoryReturnsExpectedObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsExpectedObject() { $this->assertInstanceOf(DataCash_Dpg_Model_Config, $this->_object, Unexpected objectreturned by factory); } // mageSingletonFactoryReturnsExpectedObject /** * mageSingletonFactoryReturnsTheSameObject * @author Alistair Stead * @test */ public function mageSingletonFactoryReturnsTheSameObject() { $this->assertSame($this->_object, Mage::getSingleton(dpg/config), Two different objectshave been created); } // mageSingletonFactoryReturnsTheSameObject
  • /** * 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 );} // getTransactionTypesReturnsArray
  • /** * 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 );} // getTransactionTypesReturnsArray
  • HELPERS
  • /** * 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(); }}
  • /** * 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(); }}
  • /** * 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(); }}
  • /** * 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 ); } } // getDepartmentOptionsShouldReturnAssociativeArray
  • /** * 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 ); } } // getDepartmentOptionsShouldReturnAssociativeArray
  • /** * 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 ); } } // getDepartmentOptionsShouldReturnAssociativeArray
  • Okay we’re done lets go home...
  • Hold on, we have not reallydone anything new.
  • DID WE MENTIONFUNCTIONAL TESTING YET?
  • QUALITY ASSURANCE INMAGENTO EXTENSIONS Automated Functional Testing
  • ZEND_TEST?
  • MAGENTO IS CLOSELY COUPLED TOMAGE_CORE_APPLICATION
  • Coupled components cause inflexibility
  • Dependency injection is the key
  • MAGE-TESThttp://github.com/ibuildings/Mage-Test
  • Mage_Core_Model_AppIbuildings_Mage_Controller_Request_HttpTestCaseIbuildings_Mage_Controller_Response_HttpTestCaseIbuildings_Mage_Test_PHPUnit_ControllerTestCase Ibuildings_Mage_Test_PHPUnit_TestCase
  • MAGE_CORE_APPLICATION Loaded from the community code pool
  • /** * 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;}
  • /** * 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;}
  • /** * 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;}
  • /** * 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;}
  • CONTROLLER TESTSIbuildings_Mage_Test_PHPUnit_ControllerTestCase
  • /** * 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);}
  • /** * 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);}
  • /** * 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);}
  • Ibuildings_Mage_Controller_Request_HttpTestCase Model View ControllerIbuildings_Mage_Controller_Response_HttpTestCase
  • Ibuildings_Mage_Controller_Request_HttpTestCase Model View ControllerIbuildings_Mage_Controller_Response_HttpTestCase
  • Mage_Core_Model_AppIbuildings_Mage_Controller_Request_HttpTestCase Model View ControllerIbuildings_Mage_Controller_Response_HttpTestCase
  • <?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}
  • <?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}
  • <?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}
  • /** * 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);} // theIndexActionDisplaysLoginForm
  • /** * 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);} // theIndexActionDisplaysLoginForm
  • /** * 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);} // theIndexActionDisplaysLoginForm
  • /** * 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);} // theIndexActionDisplaysLoginForm
  • /** * 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);} // theIndexActionDisplaysLoginForm
  • /** * 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.);} // submittingInvalidCredsShouldDisplayError
  • /** * 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.);} // submittingInvalidCredsShouldDisplayError
  • /** * 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.);} // submittingInvalidCredsShouldDisplayError
  • /** * 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.);} // submittingInvalidCredsShouldDisplayError
  • /** * 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.);} // submittingInvalidCredsShouldDisplayError
  • /** * 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);} // submittingValidCredsShouldDisplayDashboard
  • /** * 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);} // submittingValidCredsShouldDisplayDashboard
  • 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}
  • 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}
  • 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}
  • TESTING EMAIL CONTENTWho gets all the emails sent to test@example.com?
  • 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); }
  • 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); }
  • /** * 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); } // submittingForgotPasswordWithValidEmailReturnsSuccess
  • /** * 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); } // submittingForgotPasswordWithValidEmailReturnsSuccess
  • /** * 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); } // submittingForgotPasswordWithValidEmailReturnsSuccess
  • /** * 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); } // submittingForgotPasswordWithValidEmailReturnsSuccess
  • SO WHAT DO AUTOMATED TESTS GIVE US?
  • CODE COVERAGE
  • STATIC CODE ANALYSIS
  • CONTINUOUS INTEGRATION
  • SO WHAT NOW?
  • 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 testing
  • MAGE-TESThttp://github.com/ibuildings/Mage-Test
  • THANK YOU!• Email: astead@ibuildings.com• Skype: astead-ibuildings• Twitter: @alistairstead
  • 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/
  • IMAGE CREDITSBA 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.jpgEleventh 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/
  • QUESTIONS?
  • WE ARE HIRING!http://www.ibuildings.co.uk/about/careers/