Insights

Validating Magento Extensions with PHPUnit

PHPUnit is an open-source Unit Testing Framework by Sebastian Bergmann.

In combination with PHP 5.3’s powerful Reflection API and careful attention to class design, it can be used to validate custom Magento extension code.

The Art of Writing Testable Code

Magento can present unique challenges to conventional unit testing. The reason for this is that most of the Magento code base has a dependency on Mage, a global class that provides service location, event dispatching and many helper methods. These hard dependencies are readily recognizable:

class My_Module_Model_SomeClass
{
    public function getSomeService()
    {
        return Mage::getModel('some/service');
    }
}

The method getSomeService is coupled to Mage. This method is challenging, because we are unable to isolate the class for testing. But the hard dependency on Mage is not a requisite when developing Magento extensions. We can make this class unit test friendly by making two minor changes:

1) Use Properties with Getters

Add a some service property to the class and only request some service from Mage if it does not already exist in the class:

class My_Module_Model_SomeClass
{
    protected $_someService;

    public function getSomeService()
    {
        if(null === $this->_someService){
            $this->_someService = Mage::getModel('some/service');
        }
        return $this->_someService;
    }
}

While this change may seem minor, it can make a huge difference when it comes to testing. Using PHP’s Reflection API, we can inject some service into the some service property, removing the hard dependency to Mage.

$prop = new ReflectionProperty('My_Module_Model_SomeClass','_someService');
$prop->setAccessible(true);
$someClass = new My_Module_Model_SomeClass();
$prop->setValue($someClass,new Some_Module_Model_SomeService());

The code above accesses the protected, normally inaccessible $_someService property and injects a My_Module_Model_SomeService instance into it. This instance will be returned by the getSomeService method, allowing us to effectively mock out the My_Module_Model_SomeService class.

2) Use Setter Methods:

class My_Module_Model_SomeClass
{
    //...

    public function setSomeService(My_Module_Model_SomeService $someService)
    {
        $this->_someService = $someService;
    }

    //...
}

With the setter, we now don’t need Reflection:

$someClass = new My_Module_Model_SomeClass();
$someClass->setSomeService(new My_Module_Model_SomeService());

When to Use Reflection, When to Use Setters

Before deciding to simply throw setters on your objects, consider the purpose of your object. What should the public API do? For most purposes, I recommend using setters, as this adheres to the inversion of control principle. The power of reflection is best left to the testing of protected methods.

Installing PHPUnit

PHPUnit is available as a PEAR package and on github. If you want to use PHP’s Reflection to access protected properties or methods you will need PHP v 5.3 or greater.

Testing Your Extension

Testing your Magento module is easy when best practices are followed with regard to your codebase structure. I like to organize my tests in a ‘tests’ directory. To use PHPUnit’s autoloader, you need to be sure to follow PEAR/PSR-0 conventions (same as Magento and Zend). This means if you have the test class My_Module_Model_SomeClassTest, the directory structure must be organized as the one below:

My/
  Module/
  .  Block/
  .  Helper/
  .  Model/
  .  . SomeClass.php
  .  controllers/
  .  etc/
  .  sql/
  .  tests/
  .  . My/
  .  .  Module/
  .  .  .  Model/
  .  .  .  . SomeClassTest.php
  .  .  bootstrap.php
  .  .  phpunit.xml

To configure your test suite, you will first need a phpunit.xml config file. the PHPUnit test runner will use these settings to configure the test environment:

< phpunit backupGlobals="false"
        backupStaticAttributes="false"
        bootstrap="bootstrap.php"
        colors="false"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        processIsolation="false"
        stopOnFailure="true"
        syntaxCheck="false"
>
    < testsuites>
        < testsuite name="My_Module">
            < directory>./My< /directory>
        < /testsuite>
    < /testsuites>
< /phpunit>

Running tests normally requires more than just an XML config file. If you look carefully, the phpunit.xml phpunit node contains a ‘bootstrap’ attribute. We direct PHPUnit to load bootstrap.php. We use this file to bootstrap the Magento environment, giving us access Magento services:

<?php
//Start Magento Environment
require_once '../../../../../Mage.php';
Mage::app();

Now we can create our test class:

<?php
class My_Module_Model_SomeClassTest extends PHPUnit_Framework_TestCase
{
    protected $someclass;

    public function setUp()
    {
       $this->someclass = Mage::getModel('my_module/someClass');
    }

    public function testSomeClass()
    {
        $this->assertInstanceOf('My_Module_Model_SomeClass',$this->someclass);
    }

    public function testProtectedMethod()
    {
       $r = new ReflectionClass('My_Module_Model_SomeClass');
       $m = $r->getMethod('_protectedMethod');
       $m->setAccessible(true);
       $result = $m->invoke($this->someclass,'some input');

       $this->assertEquals('expected output',$result);
    }
}

This test can now be run using PHPUnit’s command line test runner. Open up a console and navigate to your module’s tests directory and type the following:

#phpunit My_Module_Model_SomeClassTest

Final Comments

PHPUnit is an incredible tool, and makes testing rather easy, but when should you use it? I suggest testing difficult-to-write and mission-critical code. Case in point, suppose your module integrates Magento with a third party service, an ERP system perhaps. You should definitely write tests for your integration points, including testing failure conditions.

Generally speaking, testing 100% of your code is not feasible and only worth while if 100% of your code is mission critical. Testing block classes for example are usually not necessary as visual content is tested thoroughly while testing the user interface, both during development and in QA.