Beginning unit testing in Magento 1.x

Beginning unit testing in Magento 1.x


Unit testing is not a hot topic anymore. Long gone are the days when unit testing was gaining a lot of interest in various programming related conferences around the world. Even for the PHP community, which always picks up hot topics when they have almost cooled down, it is not even warm anymore. Nowadays, you are no longer regarded as one of the cool kids if you say you practice unit testing of your code, but instead you are questioned as a serious professional if you don't do it.

There are tons of articles and presentations about unit testing and related methodologies and quite a few good articles on how to do it in PHP. But when it comes to Magento 1.x and unit testing, you usually find yourself frantically searching the Internet for any useful piece of information. Every one of us, Magento developers that do unit test their modules, went through all that at some point and we are sure you agree with us on this one.

This article is aimed at easing the road to unit testing Magento modules for those just starting to write unit tests for Magento 1.x. It is not a complete guide on the subject, it is more like the first chapter of such a guide.

Unit testing with PHPUnit

The unit testing framework of choice for most of the PHP developers is PHPUnit ( It follows the standards imposed by other xUnit frameworks that have been developed for other programming languages, such as jUnit for Java, nUnit for C# or the cppUnit for C++. These standards reflect in the way you as a developer write and execute unit tests.

In a nutshell, using a unit testing framework comes down to:

  • installing the testing framework; 
  • adding a configuration file;
  • writing some test case classes that extend a base class provided by the framework.

Throughout the test methods, you usually use assertions of various kinds to test that the state of some instances is the one you expect at given times during code execution. The next step is to run the provided tool that looks up the test case classes and executes all the test methods they define one by one. As with all xUnit testing framework, you aim for the “green bar”, the well-deserved gift you receive as a conscientious developer that does his or her work well.

PHPUnit and Magento 1.x

When it comes to unit testing a Magento 1.x module, you quickly realize that the underlying framework simply seems to work against you. That's why unit testing was not historically a habit of Magento 1.x module developers.

The biggest reasons for which Magento 1.x is hard to test are the model/block/helper factory static methods of Mage class, among others that more or less refer to the lack of Object Oriented Design applied throughout the framework's code-base.

One simple and painful rule of unit testing is that static methods cannot be easily mocked (meaning replacing their logic with your own logic), so if one of the methods you unit test calls a static method of some class, you have no clean way of replacing its logic with yours. Therefore, your unit test will not be testing a single “unit” of code, but instead, it will test two or more, depending on what that static method itself further calls. In theory, that static method is a global state and it is a dependency you cannot wire to another implementation.

Luckily, there is a solution to this and you can write unit tests using PHPUnit for Magento modules. The solution is EcomDev's PHPUnit module for Magento 1.x, which extends PHPUnit and allows you to replace whatever instance the Mage static factory methods return when your code asks for a given model, block or helper. It also provides ways to easily test your controllers, allowing you to configure the environment in which the action is executed before you dispatch the action in test. Other handy reflection utilities are also provided; they make your life easier when you want to test protected or private methods or when you have to ensure that a property with restricted visibility has a certain value.

Hands-on unit testing

You can use unit testing through Test Driven Development, which is a software development process that says you should write tests upfront and then write just as much code as needed to make the tests pass. Basically, when you assess a task, you must first translate the specifications into tests and then fill in the blanks in the production code to make all tests green, repeating this cycle until you have implemented the whole feature.

Although building software this way has a lot of advantages, such as not writing code you don't need, it is somewhat difficult to follow this methodology. Mainly because of the way we all learned to write code, that is, in a top-down approach on the production code itself. TDD needs practice and the ability to foresee the whole flow to be implemented in its very detail, and this doesn't often fit your usual Magento project.

However, it’s not the end of the world if you do not practice TDD and you still can (and also should) write unit tests for the code you write, even if you do this after you write the code. Tests written after you have the code are still valuable and you can write them as regression or integration tests.

Writing tests for a code-base you didn't write is also a good way to get to better know the code you work with and spot bad code smells and opportunities to do refactoring, such as tightly coupled units of code.

Installing prerequisites

For the scope of this article, we are using a Magento CE 1.9 installation, to which we add a simple module to have a code-base for our tests.

In order to be able to run unit tests on our Magento installation, we need to install some prerequisites and do some setup.

Here are the steps:

1. install composer;
2. create a composer.json file for installing PHPUnit and EcomDev module;
3. setup PHPUnit by creating the phpunit.xml configuration file;
4. create the bootstrap file for PHPUnit;

1. Install Composer

Installing Composer is pretty straightforward and the tutorial on the tool's website is very easy to follow. See for details on how to install Composer to your project directory.

2. Create composer.json file

After we’ve successfully installed Composer, we need to add the composer.json file that will configure what packages we will add to our Magento installation. We add the file to the project's root directory.

Installing Magento modules with Composer requires installing an extra plugin for composer, namely the magento-hackathon/magento-composer-installer module, which is hosted on the composer repository. The same composer repository also stores the Ecomdev module that makes Magento 1.x compatible with PHPUnit, namely ecomdev/ecomdev_phpunit.

Here are the contents of our composer.json file:

 "description": "My Magento 1.x Project",
 "repositories": [
 "require": {
   "magento-hackathon/magento-composer-installer": "1.*"
 "require-dev": {
   "phpunit/phpunit": "4.*",
   "phpunit/php-invoker": "1.1.*",
   "phpunit/dbunit": "1.4.*",

   "ecomdev/ecomdev_phpunit": "*"

A few notes about the contents of the composer.json file:

  • We add a third party composer repository configuration for
  • We specify that the Magento root directory is the directory where the composer is run. This setting is read and used by the Magento composer installer.
  • In the end we write our requirements in terms of packages, the composer installer as a general requirement, and the following packages as development requirements: the PHPUnit packages and the Ecomdev Magento module. We'll use PHPUnit version 4.x, an old version of the tool that is compatible with Magento 1.x and the old PHP 5.3 version that some Magento 1.x sites still run on.

After we set up the composer.json file, all we have to do is to run:

> php composer.phar install

then wait for the tool to download the dependencies and install them.

You can find more on using Composer with Magento 1.x in this very good tutorial by Alan Storm:

3. Create phpunit.xml file

Although you can run PHPUnit without a phpunit.xml configuration file, it’s easier to write all the configuration options to a file instead of writing them as command arguments each time you run the tests. Therefore, we will create a simple phpunit.xml file with only the basic configuration that would allow us to run our tests easily. Normally, PHPUnit looks for the XML file in the current directory from where you run the phpunit command, so we will add the file to our project root directory.

The contents of our phpunit.xml file are:



It’s important to set backupStaticAttributes attribute to false to avoid a series of problems regarding the serialization of some of the objects in Magento core. Doing the backup of the static attributes means that PHPUnit will try to serialize every static attribute of every object it encounters and recreate the initial state after tests. Magento classes have some attributes that are not serializable, so you'll get a lot of nasty errors if you forget to set this flag to false.

Another important setting here is the bootstrap attribute, which specifies the bootstrap file that PHPUnit will require at startup. The bootstrap file is very important because it lets us register all needed class autoloaders and do other settings. More on this file later, just bare with me.

In the listeners section we need to add one that will allow the integration of PHPUnit with the EcomDev module.
At the end of the file we define a suite of tests, which refers to the directory of our module that we want to unit test. This will allow us to run at once just the tests for our module by only passing the argument --testsuite to phpunit.


> phpunit --testsuite ProductNotes

Normally, the PHPUnit’s so-called binary file phpunit is symlinked by Composer in ./vendor/bin directory, but you can create a symlink for convenience either in the project root or in the /usr/bin directory to be able to call it from anywhere.

4. Create bootstrap file

The last step in our prerequisites setup is to create the bootstrapping source file for PHPUnit. Here is the contents of our bootstrap file, phpunit_bootstrap.php:

// Add the path to "lib" directory to the include path

// Make sure the composer autoloader is used
require_once 'vendor/autoload.php';

// Make sure the EcomDev bootstrap is ran
require_once 'app/code/community/EcomDev/PHPUnit/bootstrap.php';

The code above speaks for itself, but let's quickly iterate what it does: it makes sure that the (legacy) code libraries that are placed in the ./lib directory of our installation are loadable when running tests, then it requires the autoloader generated by Composer to be able to load the classes for the packages we installed through Composer and, at the end, it includes the bootstrap file that Ecomdev module defines to initialize the Magento application and unregister the Magento autoloader.

Introducing our module

We created a small module especially for this guide, and it is named Evo_ProductNotes. It is a small module which allows site visitors to save notes about products. The need for such a functionality is questionable and the module is clearly not prepared for use in a production environment as it stands now, but it does a hell of a good job being the punching bag for this guide on unit testing.

The module has something for everyone:

  • it has a product note model class that models the product note, along with the resource model for it;
  • it defines a collection of note model instances;
  • it has a controller that provides an action for creating a new note about a product;
  • it also makes use of a helper that returns context data such as current product and current datetime;
  • and, of course, it defines blocks for displaying a form to add a new note's content and the list of already added notes.

In the frontend, the module's layout updates simply add a new tab in the product page description area and displays there the list of notes and the form to add a new note.

You can consult the complete code for the module here.

Testing our module

The tests we've written for the product notes are pretty basic and are nowhere near a complete suite of tests that you would normally add to the modules you develop for real projects. Please take them as the bare minimum you need to write to be able to test your code. More detailed examples with specific scenarios will follow in a future post.

Some theory first

Before we jump into analyzing examples of tests, it's a good thing to review some of the methods PHPUnit and especially EcomDev module provide us for writing our tests.

Throughout our examples we will use:

  • assertions;
  • expected exceptions;
  • data providers;
  • expectations;
  • test dependencies;
  • mock objects.

An "assertion" is a statement that a predicate is expected to always be true at a specific point in the code. PHPUnit provides a large set of assertion methods in the test case class that you can use to test the state of various objects or return of methods.

Arguably the most used assertions are:

  • assertEquals($expected, $actual) – asserts that the two passed values are equal;
  • assertSame($expected,$actual) – asserts that the two passed values are identical;
  • assertTrue($condition) – asserts that the condition given as argument evaluates to true;
  • assertFalse($condition) – asserts that the condition given as argument evaluates to false;
  • assertCount($expectedCount, $haystack) – asserts that the haystack argument contains exactly the expected count of elements;
  • assertEmpty($actual) – asserts that the passed argument is empty;
  • assertNotEmpty($actual) – asserts that the passed argument is not empty.

The complete list of assertions can be found here:

PHPUnit allows you to specify that a given test must throw an exception of a given type, optionally having a given message and a specified code. This comes in very handy when you want to test that your code properly verifies preconditions and throws meaningful exceptions if the preconditions are not met.

There are two ways of telling PHPUnit that you expect an exception to be thrown during test execution:

  • by using annotations in the test method's documentation block – there is the main annotation @expectedException, which must be followed by the name of the exception class, and other optional annotations, such as @expectedExceptionMessage, @expectedExceptionMessageRegExp and @expectedExceptionCode;
  • by calling the expected exception setter in the method body before calling the code tested for throwing exception.


$this->setExpectedException('InvalidArgumentException', 'The message', 50);

PHPUnit allows you to run each of your tests with several sets of test data with the use of data providers. In order to make use of this feature, all you have to do is add a @dataProvider annotation to the method's doc block followed by either the name of a public method of the same test case class or the string “dataProvider”.

In the first use case it expects that the method identified by its name will return an iterable data structure with array elements, each of the array elements being the list of arguments for the test method for a test run. In the second case it will look for test data in a YAML file in a directory named “providers”, relative to the test case class. In both cases it will run your test method with each of the data sets.

You also have the option of storing the values you expect during tests, values that you normally use in assertions, outside of the test case class, in a YAML file. This is especially useful when you expect a function that you test to return a large array structure that would take up too much space in the test case class as part of a data provider method. You can separate the entire expected data structure from the code by extracting it in a YAML file with the name of the test method that you place in an “expectations” directory relative to the test case class.

We will provide examples with YAML data provider and expectations files later.

PHPUnit also allows you to define dependencies between tests. That means you can run one test after another and make it run only if the previous one was successful. It also allows you to pass the return of the first test as a parameter for the second one. We will have a short example of dependent tests later.

Most of the time you will want to test your code in isolation. That's why they call it unit testing after all. But object oriented programming, in essence, means that objects communicate between themselves, and they do this by calling each other’s methods. In order to achieve the desired isolation in a test, you need to make use of the so called stubs and mocks.

Technically speaking, the difference between a stub and a mock is that a stub is like a placeholder for an object with methods that just return predefined values without any expectations regarding the caller code, whereas the mock is an object pre-programmed with expectations regarding the calls on its methods in terms of the number of times a method is called, the arguments passed to it each time and, of course, its return.

EcomDev's module enables mocking objects in Magento 1.x by allowing you to replace the objects that Mage factory methods return when the tested code calls them for models, resource models, model singletons, helpers and blocks. All this power comes from a single method you inherit in your test case class when you extend the base test case class EcomDev_PHPUnit_Test_Case, namely the replaceByMock($type, $classAlias, $mock) method.

The $type argument can take one of the following values: "model", "helper", "block", "singleton", "resource_model". The $classAlias is the so-called grouped class name you usually pass to factory methods of Mage class and the third argument is the mock object built to replace the factory returned instance. The examples in our module will show you how this works in practice.

As a conclusion to the short walk-through, the tools you have for testing a unit of code are the assertions that you apply to the return values of a given method, or on the state of a given object after the method was called, and the expectations you define in the mocks you wire the tested unit of code to. So, in a nutshell, you can test method returns, their side effects and their interaction with other objects.

Some example tests from our module

The examples we'll provide below first present the code that is targeted by the test and then the unit test code with some explanations in each of the cases. The documentation comments were stripped from the presented code where they were not necessary for the scope of the example.

Testing the model:
The code to test

Class: Evo_ProductNotes_Model_Note

  public function getNoteHtml()
    return nl2br($this->getData('note'));

  public function setNote($note)
    $clean = strip_tags($note);
    $length = mb_strlen($clean);

    if ($length == 0) {
      throw NoteException::forEmptyNote();

    if ($length > $this->getMaxNoteLength()) {
      throw NoteException::forMaximumLengthExceeded($length, $this->getMaxNoteLength());

    $this->setData('note', $clean);

Test that the content of the note is properly sanitized and then formatted for display as HTML
Class: Evo_ProductNotes_Test_Model_NoteTest

   * @test
   * @dataProvider dataProvider
    public function setNoteThenGetNoteHtml($rawContent, $expectation)
      $note = Mage::getModel('evo_productnotes/note');
      $actualContent = $note->getNoteHtml();

      $expectedContent = $this->expected($expectation)->getExpectedContent();
      $this->assertEquals($expectedContent, $actualContent);


  • We are using the @test annotation in this case so that PHPUnit knows that this method is a test and it should run it accordingly. Usually, you'll omit this annotation and will prefix the methods that are tests in a test case class with “test”. You will see plenty of such examples in the remainder of this article.
  • We are using a data provider that will get the test data from an external file; please note the “dataProvider” value attached to the annotation.
  • The provider file content is given below.
  • We’re also using expectations that we load from an external file. The contents of the file are provided below.
  • We are covering two methods with this test, the setNote() setter and the getNoteHtml() getter.
  • The test simply asserts that the value returned by the getter was filtered and formatted according to the specification.

File: ./providers/setNoteThenGetNoteHtml.yaml

"note contains just plain text no new lines":
  - "This is the note content"
  - "expectation_1"

"note contains just plain text with new lines":
  - |
    This is the note content
    This is the second line
  - "expectation_2"

"note contains html all on one line":
  - "

Hey there people, how are you?

" - "expectation_3" "note contains html with new lines": - |

This is the note content

This is the second line - "expectation_4"

File: ./expectations/setNoteThenGetNoteHtml.yaml

  expected_content: "This is the note content"

  expected_content: |
    This is the note content
This is the second line expectation_3: expected_content: "Hey there people, how are you?" expectation_4: expected_content: | This is the note content
This is the second line

Test that the setNote() method throws exception when the input is not valid
Class: Evo_ProductNotes_Test_Model_NoteTest

  * @test
  * @dataProvider dataProvider
  public function setNoteFails($rawNote, $exceptionExpected, $maxNoteLength = 0)
    // Mock a note model instance
    $noteModelMock = $this->getModelMock('evo_productnotes/note', ['getMaxNoteLength']);
    // Set expectancies and return for mocked method

    // Setup the expected exception
    $exceptionExpected['message'], $exceptionExpected['code']);

    // Call tested method

And the test data taken from ./providers/setNoteFails.yaml :

"empty note content":
  - ""
    "type": "Evo_ProductNotes_Model_Note_Exception"
    "message": ""
    "code": 1

"note content too long":
  - "This note is too long"
    "type": "Evo_ProductNotes_Model_Note_Exception"
    "message": "Note content length 21 exceeds maximum allowed length of 10"
    "code": 2
  - 10


  • The data provider file contains the content to pass as the first argument to the test method, then the exception details passed as an associative array in the second argument. The third argument is optional and it is used as the return of the mocked getMaxNoteLength() method.
  • setExpectedException() is called from within test to setup the expected exception. Using this instead of annotations gives us more flexibility in writing expectancies based on input.

Test with two dependent tests (alternative to testing a flow in one test)

  * @return Evo_ProductNotes_Model_Note
  public function testSetNote()

This is the note content

\nThis is the second line"; $note = Mage::getModel('evo_productnotes/note'); $note->setNote($noteContent); return $note; } /** * @param Evo_ProductNotes_Model_Note $note * @depends testSetNote */ public function testGetNoteHtml($note) { $expectedContent = "This is the note content
\nThis is the second line"; // Call tested method $actualContent = $note->getNoteHtml(); // Do the equality assertion $this->assertEquals($expectedContent, $actualContent); }


  • The second test depends on the first one, the $note argument for the second one will be the return of the first one, if the first one will succeed.
  • If the first test fails, then the second one will be skipped.
  • There is a downside: the test on which another depends cannot have a data provider, so you cannot run it with more than one set of data, as in the example above.
  • Anyway, the second test can also be configured to use a data provider, in which case the elements in the data provider return are taken as the first arguments of the test method and the return of the previous test is the last.
  • You can also make a test depending upon more than one other test, all the returns being available as arguments to the dependent test method.

Testing the controller

Controller class: Evo_ProductNotes_NoteController

public function newAction()
  $request = $this->getRequest();

  // Validate POST parameters
  $productId = $request->getPost('product_id');

  // Check for product_id parameter to be present
  if (is_null($productId)) {
    throw MissingParameterException::forMissingPostParameter('product_id');

  // Check if product_id is of correct type
  if (!is_numeric($productId)) {
    throw WrongParameterException::forWrongPostParameter('product_id');

  $content = $request->getPost('note', '');
  // Check the cote parameter to not be missing or empty
  if (strlen($content) === 0) {
    throw MissingParameterException::forMissingPostParameter('note');

  // Create note model, fill in data and then save
  $note = Mage::getModel('evo_productnotes/note');
  $helper = Mage::helper('evo_productnotes');



Test the happy flow of the action that saves a new product note
Class: Evo_ProductNotes_Test_Controller_NoteControllerTest

  * Tests the success flow for the action.
  * @param array $postData
  * @param string $currentDatetime
  * @param string $referer
  * @dataProvider newActionProvider
  public function testNewAction(array $postData, $currentDatetime, $referer)
  // Setup request
  $this->setPostData($postData, $referer);

  // Mock the helper
  $helperMock = $this->getHelperMock('evo_productnotes', array('getCurrentDatetimeMysqlFormatted'));
  $this->replaceByMock('helper', 'evo_productnotes', $helperMock);

  // Mock the note model
  $noteMock = $this->getModelMock('evo_productnotes/note',

  // Set the expectancies regarding the setters of the model and the save method


  // Replace the model Mage returns with our mock
  $this->replaceByMock('model', 'evo_productnotes/note', $noteMock);

  // Dispatch action

  // Expect action to redirect to referrer


  * Sets POST data and prepares request object for test case
  * @param array $postData
  * @param string $referrer
  protected function setPostData(array $postData, $referrer = null)
    $request = $this->getRequest();
      if ($referrer) {
            Mage_Core_Controller_Varien_Action::PARAM_NAME_REFERER_URL, $referrer);
  * Data provider
  * @return array
  public function newActionProvider()
    return [
      [['product_id' => 1, 'note' => 'some text'], '2016-01-01 10:00:00', Mage::app()->getStore()->getBaseUrl() . '/some-path']


  • For testing a controller action you must extend the class EcomDev_PHPUnit_Test_Case_Controller. It provides a series of helper methods that you can use to setup the testing environment and dispatch the action at test.
  • We test that the action sets correct data on the note model and saves it at the end, and also that the action redirects to the referrer at the end.
  • This time we use an in-class data provider, newActionProvider(), that returns just one set of test data. We might add others as the requirements evolve.

Test the exceptional flows of the action that saves a new product note

  * @dataProvider newActionThrowsExceptionProvider
  public function testNewActionThrowsException(array $postData, $exceptionExpected)


    $this->setExpectedException($exceptionExpected['type'], $exceptionExpected['message'], $exceptionExpected['code']);


  * Data provider
  * @return array
  public function newActionThrowsExceptionProvider()
    $typeMissing = 'Evo_ProductNotes_Controller_Exception_MissingParameterException';
    $typeWrong = 'Evo_ProductNotes_Controller_Exception_WrongParameterException';

    // Message templates and codes are defined as constants in the exception classes
    $messageMissing = Evo_ProductNotes_Controller_Exception_MissingParameterException::MESSAGE_POST_PARAMETER;
    $messageWrong = Evo_ProductNotes_Controller_Exception_WrongParameterException::MESSAGE_POST_PARAMETER;

    $codeMissing = Evo_ProductNotes_Controller_Exception_MissingParameterException::CODE_POST_PARAMETER;
    $codeWrong = Evo_ProductNotes_Controller_Exception_WrongParameterException::CODE_POST_PARAMETER;

    // Return test data for 4 runs
    return [
      [['note' => 'some text'], ['type' => $typeMissing, 'message' => sprintf($messageMissing, 'product_id'), 'code' => $codeMissing]], // missing product_id
      [['product_id' => 'abc', 'note' => 'some text'], ['type' => $typeWrong, 'message' => sprintf($messageWrong, 'product_id'), 'code' => $codeWrong]], // wrong product_id type
      [['product_id' => 1], ['type' => $typeMissing, 'message' => sprintf($messageMissing, 'note'), 'code' => $codeMissing]], // missing note
      [['product_id' => 1, 'note' => ''], ['type' => $typeMissing, 'message' => sprintf($messageMissing, 'note'), 'code' => $codeMissing]], // empty note


  • This example clearly shows how you can expect different exception types to be thrown by the code you test.
  • Although the testing framework allows you to expect a message and a code for the exceptions, usually it is good enough, and maybe better, to expect just the exception to be of a given type as the message itself can vary based on locale or other reasons and also the code may change in time. Therefore, by expecting just the exception type, you are flexible enough to handle different exceptional cases in different ways but, at the same time, you are not coupling your test to the actual implementation details too much.

Closing notes

This post was written with the intention of guiding you throughout the process of unit testing your Magento 1.x code. We went through the steps you have to take in order to install and setup the testing framework in your project, then we discussed some of the features of PHPUnit.

Some examples backed up all our explanations regarding writing unit tests. If you are new to unit testing, I bet you now feel more prepared for the challenge ahead of you. That’s the spirit!

However, this is just the beginning. There are a lot more features of PHPUnit and also of the EcomDev module. There is still a lot to explore and talk about, and you’ll have the pleasure to find all that out in a future post.


Tell us what you think

Fields marked with " * " are mandatory.

  • Azaz Qadir Mar. 21,2017
    Nice guide for unit testing Magento. Is the process same for all the PHP based apps? If I have a custom PHP website, do I have follow this same process for unit testing or follow some tutorial (like this one: ) to do it?
    • Alex May. 29,2017
      Our article specifically refers to unit testing in Magento 1.x. If this is your case, you can definitely follow our instructions.

We use cookies to offer you the best experience on our website. Learn more

Got it