11

I maintain a Magento 1.9 site with several custom modules. Some of the functionality is business critical and in dire need of some unit tests. E.g. a unit price calculator.

I usually develop in Symfony and would really prefer to somehow use PHPUnit (w/Composer) if that's at all possible.

Some functionality is based on data imported to a several custom database tables so I would prefer to somehow load fixtures.

So I'm looking for the best practice approach to writing some unit tests. I will glady accept tutorials or something similar. Any help is appreciated.

10

I faced the same problem a while back.

I considered using the Ecomdev PHPUnit module but I find it hard to use and poorly documented (but I still love what Ivan does and his great contribution to the Magento ecosystem).

So, with the help of Vinai, I ended up developping the following test framework module: https://github.com/digitalpianism/testframework

Original purpose was for integration tests but I'm using it for unit tests too. You can see it in action here: https://github.com/digitalpianism/easytoplinks/blob/master/app/code/community/DigitalPianism/EasyToplinks/Test/Unit/Block/Page/Template/LinksTest.php

Regarding fixtures, I'm using transaction rollbacks to avoid creating sample data in database.

  • This looks really promising. I'll try it out. Thanks. – frigg Nov 3 '16 at 14:15
13

Installation

Since Magento 1 doesn't use composer out of the box, I don't think it makes a big difference if you install phpunit using composer or just download the phar version.
If you already use composer to manage other third party modules or libraries in your site, then composer probably makes the most sense. Unless you use PHP7 you will be limited to an old version of phpunit though (that's why I linked to the 4.8 version above).

Integration Tests vs/and/or Unit Tests

Since Magento 1 is such a heavy weight application, it makes sense to separate the phpunit bootstrap into one for integration and one for unit tests.
The unit test bootstrap only needs to initialize the autoloader, while the integration test bootstrap needs to initialize the whole application environment including configuration loading and the db connection.
Because of that integration tests in Magento tend to run a lot slower then unit tests (even more so then in other applications).

Bootstrapping Magento into phpunit

  • The autoloader of Magento is not PSR-0 compliant as it throws an exception if it can't find the file a class is located in. This breaks some usages of class_exists in phpunit. There are several possible (if hacky) workarounds:

    • Unregister the Magento autoloader, wrapping \Varien_Autoload::autoload() in a decorator ignoring exceptions thrown within, and register the wrapper as a new autoloader. This has a low chance of conflicts with third party libraries who register autoloaders and depend on a specific autoloader order.
    • Use a custom error handler wrapping the one built into Magento 1. The custom error handlers swallows errors triggered by the Magento autoloader. This is the solution that Raphael's test framework uses. This seems to be the most compatible with other third party extensions.
    • Use the include path hack to override \Varien_Autoload::autoload() to not throw the error if the file doesn't exist. This however conflicts with several modules that also override the same class. I don't use this approach myself.
  • To avoid errors from the session being started during tests simply set $_SESSON = [] in the bootstrap.

  • Set a custom response object via Mage::app()->setResponse($testResponse) that extends the real one but does not send output or headers.

  • To reinitialize Magento between integration tests that completely change the runtime state, use Mage::reset(); Mage::app(). Note that after that the error handler will have to be re-decorated.

Fixtures

For DB fixtures I tend to use the regular models in fixture methods to create fixtures, e.g. createSimpleProduct($sku). Like Raphael said, use setUp() and tearDown() to wrap the test in a transaction that gets rolled back after the test (for example Mage::getSingleton('core/resource')->getConnection('default_setup')->beginTransaction()).

For store configuration fixtures, I tend to set up in-memory only fixtures using Mage::app()->getStore()->setConfig($path, $value).

The EcomDev_PHPUnit extension also provides the option to create DB fixtures using yaml files, but for myself I find those harder to maintain compared to fixtures created using model classes. YMMV.

Test Doubles

The registry can be used to inject test doubles for objects created via Mage::getSingleton(), Mage::getResourceSingleton() and Mage::helper().
Some other central objects can be set on Mage::app() (e.g. the request).
To replace classes created via Mage::getModel() or Mage::getResourceModel() with test doubles, a custom config object wrapper has to be used. See this example in Raphael's test framework how that can be accomplished.

Summary

Once Magento is bootstrapped, pretty much everything can be tested rather nicely. Be prepared to create deep mocks because of the large amount of method chaining the core code uses though.
Even though the setup is hacky, it works well and I find the tests give me a lot of confidence and value, pretty much comparable to a test suite for a Symphony app.

  • I never tryed it but why not use Magento Test Framework ? (docs.magento.com/m1/ce/user_guide/magento/…) – Fra May 6 '17 at 6:41
  • 3
    Yes I've tried it, but it's functional testing (not unit or integration), it's slow, it's complex and tests tend to be flaky and brittle. All in all I considered the time I spent with it a waste. – Vinai May 6 '17 at 12:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.