EcomDev PHPUnit Tip #10

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #10: Broken Translations

It happens that translations are not working in unit tests if you replace a helper with a mocked helper, using mockHelper() and replaceByMock(), especially in developer mode where translations only work if defined for the right module.

The module name is determined in Mage_Core_Helper_Abstract by $this->_getModuleName(), so let’s look into this method:

/**
 * Retrieve helper module name
 *
 * @return string
 */
protected function _getModuleName()
{
    if (!$this->_moduleName) {
        $class = get_class($this);
        $this->_moduleName = substr($class, 0, strpos($class, '_Helper'));
    }
    return $this->_moduleName;
}

If your helper class is Your_Awesome_Helper_Data, the mocked helper class will actually be Mock_Your_Awesome_Helper_Data. As a module named Mock_Your_Awesome doesn’t exist, nothing can be translated.

Solution

To make your helpers unit test proof (and as a side effect also rewrite proof), define _moduleName explicitly:

class Your_Awesome_Helper_Data extends Mage_Core_Helper_Abstract
{
    protected $_moduleName = 'Your_Awesome';
}

First described here: http://magento.stackexchange.com/questions/46255/ecomdev-phpunit-translation-not-working-with-mocked-helper

EcomDev_PHPUnit Tip #9

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #9: Checkout Test

3 Years ago I already wrote an article about how to write a checkout integration test. But the practices used there are not longer up to date and some workarounds have become unnecessary. This post shows what’s necessary to write a test that simulates the Magento checkout, using techniques learned in Tip #1.

  1. Since there are some singletons involved, make sure to reset their state:
    /*
     * @test
     * @singleton checkout/session
     * @singleton customer/session
     * @singleton checkout/cart
     */
    
  2. You should visit the cart page once to trigger totals collection. Assuming, the customer id is 1 and she has an active quote (from previously added products in the test or from a quote fixture), you start with:
    $this->customerSession(1);
    $this->dispatch('checkout/cart');
    
  3. Before each new request, you have to reset the checkout session singleton manually within the test, otherwise the quote is not reloaded and you might even lose it completely:
    Mage::unregister('_singleton/checkout/session');
    $this->customerSession(1);
    $this->dispatch('checkout/onepage');
  4. Sometimes you want to logout a customer with active shopping cart. This needs three steps:
    Mage::getSingleton('customer/session')->logout();
    Mage::getSingleton('checkout/cart')->unsetData();
    $this->guestSession();

EcomDev_PHPUnit Tip #8

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #8: “sort_order is ambiguous” Error

Zend_Db_Statement_Exception: SQLSTATE[23000]: Integrity constraint violation: 1052 Column ‘sort_order’ in order clause is ambiguous

If you get this MySQL error in your tests, you probably forgot the attribute set in your EAV fixtures. attribute_set_id should always be set, even for customers and addresses (just use “0” there)

EcomDev_PHPUnit Tip #7

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #7: YAML Directory Fallback

YAML files for fixtures, expectations and data providers are expected in the following scheme by default, where in this example Importer.php contains the test case and testImport is the test method:

│   Importer.php
│
├───Importer
│   ├───expectations
│   │       testImport.yaml
│   │
│   ├───fixtures
│   │       testImport.yaml
│   │
│   └───providers
│           testImport.yaml

Here the signature with annotations:

    /**
     * @param string $csv CSV file content
     * @test
     * @loadFixture
     * @loadExpectation
     * @dataProvider dataProvider
     */
    public function testImport($csv)

Instead of the method name the file name of the YAML file can also be stated explicitly, like in this example where another test also uses fixtures/testImport.yaml (how this also works for data providers, see Tip #3).

    /**
     * @param string $csv CSV file content
     * @test
     * @loadFixture testImport
     * @dataProvider dataProvider
     * @expectedException RuntimeException
     */
    public function testFailingImport($csv)

But even the directory structure doesn’t have to be used in this form. If EcomDev_PHPUnit doesn’t find the according file in this place, the following fallback hierarchy is used:

  1. Default: see above
  2. Module: The directories “fixtures”, “expectations” and “providers” are searched directly in the “Test” directory of the module
  3. Global: The directories are searched in any parent directory of the test case up until the Magento root

This way you can use fixtures etc. across multiple test cases. Today I prefer the second variant for module wide definition in most cases, at least for fixtures.

Incidentally, the hierarchy is defined in the EcomDev_PHPUnit config.xml as follows:

<config>
    <phpunit>
        <suite>
            <yaml>
                <model>ecomdev_phpunit/yaml_loader</model>
                <loaders>
                    <default>ecomdev_phpunit/yaml_loader_default</default>
                    <module>ecomdev_phpunit/yaml_loader_module</module>
                    <global>ecomdev_phpunit/yaml_loader_global</global>
                </loaders>
            </yaml>
        </suite>
    </phpunit>
</config>

These loaders are models which inherit from EcomDev_PHPUnit_Model_Yaml_AbstractLoader, so with an own module you theoretically can add own custom loaders to use arbitrary sources for the YAML files.

EcomDev_PHPUnit Tip #6

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #6: Fixture Rollback

If you use fixtures, the fixture data is removed from the database after the corresponding test finishes. But it’s important to note, that there is no transaction rollback, instead all tables affected by the fixture will be truncated after the test.

This has some implications:

  • You shouldn’t ever use table fixtures with tables that contain important core data, like core_config_data or eav_attribute
  • Data in tables, not contained in the fixture, stay. You can trigger deletion of data created during test with an empty fixture, for example for quotes and orders:
    tables:
      sales/quote: []
      sales/order: []
    
  • If this behavior is not desired, you should clean up behind yourself, i.e. delete created data in a tearDown method.
  • Don’t rely too much on data present in the original database, as other tests might overwrite and eventually delete it with their fixtures
  • If you use shared fixtures with @loadSharedFixture all data in the shared fixture is only created and removed once. All tables affected by a shared fixture will not be cleaned up in between at all, even if other normal fixtures add data. My advice: Use shared fixtures with caution, or better not at all.

EcomDev_PHPUnit Tip #5

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #5: Secure Area

Problem: test cases extending EcomDev_PhpUnit_Test_Case_Controller with customer fixture fail with Cannot complete this operation from non-admin area because Magento is in area=frontend mode during tearDown and does not allow deletion of customers. The same problem occurs when you try to delete customers in a test outside of an adminhtml context.

The customer model checks if the isSecureArea flag is set in the Magento registry, so to solve the problem, we set this flag in the test. There are two possible ways to do it:

1.) If you create and delete customers within the test:

/*
 * @test
 * @registry isSecureArea
 */
public function testThatNeedsToDeleteCustomers()
{
    Mage::register('isSecureArea', true);
    // ...
}

(note the @registry annotation which will reset the flag afterwards, see Tip #1)

2.) If you use a customer fixture:

protected function setUp()
{
    Mage::register('isSecureArea', true);
    parent::setUp();
}
protected function tearDown()
{
    parent::tearDown();
    Mage::unregister('isSecureArea');
}

or if you use class wide fixtures:

    public static function setUpBeforeClass()
    {
        Mage::register('isSecureArea', true);
    }
    public static function tearDownAfterClass()
    {
        Mage::unregister('isSecureArea');
    }

EcomDev_PHPUnit Tip #4

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #4: Named Parameters

The advantage of YAML files for configuration is supposed to be easy readability. But if a data provider looks like this, there’s not so much readability left:

-
  - 7
  - 1
  -
    5: 7
    6: 9
-
  - 7
  - 1
  -
    5: 8
    6: 10

Technically the same, but way more maintainable:

Bundle_X_A1_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 7
    6: 9
Bundle_X_A2_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 8
    6: 10

You can at least guess, that this one is about adding bundle products to the cart, and if you know it, the test data is easy to understand, without having to look at the source code.

Another positive effect is, that for failed tests PHPUnit doesn’t show messages like TestCase::test() with data set #1 anymore, but for example TestCase::test() with data set "Bundle_X_A1_B2"

EcomDev_PHPUnit Tip #3

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #3 Shared Data Providers

Did you ever wonder, why you can specify the file name for expectations and fixtures, but for data providers you seem to be limited to the default “test name dot yaml”? The simple reason is, @dataProvider is a native feature of PHPUnit and its parameter actually must be a method name.

So @dataProvider dataProvider means, the method EcomDev_PHPUnit_Test_Case::dataProvider() is used as data provider:

    /**
     * Implements default data provider functionality,
     * returns array data loaded from Yaml file with the same name as test method
     *
     * @param string $testName
     * @return array
     */
    public function dataProvider($testName)
    {
        return TestUtil::dataProvider(get_called_class(), $testName);
    }

But EcomDev_PHPUnit also has a way to specify the file name for the data provider explicitly, thus allows shared providers:

/**
 * @dataProvider dataProvider
 * @dataProviderFile customFileName.yaml
 */
public function testSomething($something)

EcomDev_PHPUnit Tip #2

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #2: Expectation Keys

This is a short one: expected('works %s sprintf', 'like').

Which expectations should be loaded, usually depends on input data, so if your expectation file looks like this:

- product-1-qty-10:
  - answer: 42
- product-2-qty-10:
  - answer: 42

you can load it in the test like this:

/**
 * @test
 * @loadExpectation
 * @loadFixture
 * @dataProvider dataProvider
 */
public function testSomething($productId, $qty)
{
  $expectedAnswer = $this->expected('product-%s-qty-%s', $productId, $qty);
}

EcomDev_PHPUnit Tip #1

For years, the test framework EcomDev_PHPUnit is quasi-standard for Magento unit tests. The current version is 0.3.7 and last state of official documentation is version 0.2.0 – since then, much has changed which you have to search yourself in code and GitHub issues. This series shall collect practical usage tips.

Tip #1: Reset Global State

Something that makes testing with Magento quite difficult, is the liberal use of global state, in form of singletons and registry. They are not reset between tests, but EcomDev_PHPUnit allows explicit resetting with annotations.

/**
 * @singleton checkout/session
 * @helper tax
 * @registry current_product
 */
public function testSomething()

The parameters are the same as for Mage::getSingleton(), Mage::helper() and Mage::registry().

It is recommended to reset all singletons and registry values that are used during the test, not only after you get conflicts. Especially resetting used session singletons is important, regardless if they are mocked in the current test. Only for stateless helpers, i.e. those that don’t have own attributes, resetting is not necessary.