PHPUnit Integration with Magento

Last modified by Gabriiiel on Fri, July 2, 2010 08:55
Source|Old Revisions  

This is an old revision of the document!

TDD (Test Driven Development) is a widely accepted practice for programming today. The PHP community, however, has been slow to adopt this method of programming.

This article briefly explains how you can integrate PHPUnit‘s popular testing suite into your Magento source code. It does not provide assistance in writing test cases.



Although the directory structure you wish to use for your testing is entirely up to you, in this document we will assume you have added a directory named ‘tests’ under the Magento root.

Create a new directory under your document root (or wherever Magento has been installed) named ‘tests’. All of your unit tests will go within this directory.

Writing Your First Test Case

Setting up a test case for Magento is surprisingly simple, and involves just a small addition to the setUp() method of your test case. See the example below

  1. require_once 'PHPUnit/Framework.php';
  2. require_once '../app/Mage.php';
  4. class MyModelTest extends PHPUnit_Framework_TestCase {
  6.   public function setUp() {
  7.     Mage::app('default');
  8.     $this->myModel = Mage::getModel("mymodule/mymodel");
  9.   }
  11.   public function testGetWidgets() {
  12.     $widgets = $this->myModel->getWidgets();
  13.     $this->assertEquals(25, sizeof($widgets));
  14.   }
  16. }

As you can see above, we’ve included two external files:

  1. PHPUnit/Framework.php: No explanations should be necessary, as this is required under the standard method of writing PHPUnit test cases.
  2. ../app/Mage.php: Again, we’ve made the assumption your test case is located in a sub-directory under the Magento document root. This will allow us access to the Mage final class and provide us the means to initiate the Magento session for this test.

In the setUp() method for any test, it is required to add the following line:

  1. Mage::app('default');

That’s it! You should now be able to load models and test configuration just like you would in any Magento controller. Whether or not you chose to load your model in the setUp() method remains up to you. You should have access to the Mage model from within any test method you write.

Drawbacks, known Issues

  • To test a controller you must override the controller with a test specific subclass, you will need to override problematic magento methods to avoid coupling your tests to global static state.

Using PHPUnit+Magento with Phing

If you are using Phing to create builds and do unit testing, you may be required to modify the include for Mage.php. For instance, if your build.xml is in the root level of the Magento directory, remove the leading ‘../’ from the require_once function in each of your tasks files.

An example target for phing is given below using the structure defined earlier in this document:

  1.   <target name="test" description="runs unit tests against the magento code">
  2.     <echo msg="Executing unit tests on current code" />
  3.     <phpunit haltonfailure="true" printsummary="true">
  4.       <batchtest>
  5.         <fileset dir="./tests/">
  6.           <include name="**/*Test.php"/>
  7.         </fileset>
  8.       </batchtest>
  9.     </phpunit>
  10.   </target>

In Michael Feather’s book Working effectively with legacy code, and in xunit test patterns, (originally introduced by the extreme programmers in a paper called refactoring test code) they introduce the concept of a test specific sub-class.

Here is the test bootstrap we use for the extension:

  1. <?php
  2. error_reporting( E_ALL | E_STRICT | E_NOTICE );
  3. ini_set( 'display_errors', 1 );
  5. define( 'ELITE_PATH', 'E:devvafappcodelocalElite' ); // put path to app/code/local/Elite
  6. define( 'ELITE_CONFIG', ELITE_PATH . '/Vaf/config.ini' );
  7. define( 'ELITE_BASEURL', '' );
  8. define( 'TESTFILES', dirname( __FILE__ ) . '/../../tests/files/' );
  9. mysql_connect( 'localhost' , 'root', '' ) or die( mysql_error() );
  10. mysql_select_db( 'vaf' ) or die( mysql_error() );
  13. require_once( dirname( __FILE__ ) . '/../../../../Mage.php' );
  15. /**
  16. * Null out some Magento behavior we do not want to execute because it would have some un-intended side effects.
  17. */
  18. class Mage_Bundle_Model_Option
  19. {
  20. }
  22. class Mage_Core_Model_Url_Rewrite
  23. {
  24. }
  26. class Mage_Customer_Model_Session
  27. {
  28.     function __call( $mandatory, $argument )
  29.     {
  30.     }
  31. }
  33. class Mage_Core_Model_Session
  34. {
  35.     function __call( $mandatory, $argument )
  36.     {
  37.     }
  39.     public function init($namespace, $sessionName=null)
  40.     {
  41.         return $this;
  42.     }
  43. }

This nulls out a fair amount of stuff you probably do not want running in your tests. Also make sure to instantiate any objects you are testing, don’t ever use Mage::helper() in a test.

If you have any static dependencies in your production code to the Magento namespace, I recommend extracting methods to encapsulate that, and override that method in a test specific sub class for each test class.

I have found that I need to use a transaction rollback scheme, and use whitebox database state verification, for most Magento behavior. Something along the lines of the following. Also notice in the fixture setup we clear out the request variables, so we do not use Magento’s request object in a test. Sometimes we use the Zend one which satisfies the interface.

  1. function setUp()
  2.     {
  3.         foreach( $_GET as $key => $val )
  4.         {
  5.             unset( $_GET[ $key ] );
  6.         }
  7.         $this->startTransaction();
  8.         $this->truncateTable( 'catalog_product_entity' );
  9.         $this->doSetUp();
  10.     }
  12.     protected function doSetUp() {}
  13.     protected function doTearDown() {}
  15.     function tearDown()
  16.     {
  17.         $this->doTearDown();
  18.         $this->rollbackTransaction();
  19.     }


Brian Celenza, Sr. PHP Developer,, bcelenza at gmail dot com, Josh Ribakoff