New Version Of EcomDev_PHPUnit 0.1.2 Extension For Magento

POSTED ON Feb 10, 2011 IN in Extensions, How To

There are just 10 days were gone and we are happy to introduce new version of our Magento Unit Test suite. In this version were added full multi-store support and EAV fixtures, was improved the way of expectations retrieving and some other minor fixes.

Those, who do not know about what the post is, here is the previous article about the extension:
http://www.ecomdev.org/2011/02/01/phpunit-and-magento-yes-you-can.html

As usual extension is available at Magento Connect:
http://www.magentocommerce.com/magento-connect/phpunit-testing-integration.html

and via SVN:
https://github.com/EcomDev/EcomDev_PHPUnit

So let go over the features included in this extension…

EAV Fixtures support

Starting from this version there is available EAV fixtures loading via YAML fixture file. It is just new fixture type like, config or tables.

The structure of such a fixture looks like the following:

eav:
  catalog_product:
     # First Product
    - entity_id: 1
      type_id: simple
      sku: book
      name: Book
      short_description: Book
      description: Book
      url_key: book
      stock: # product stock item
        qty: 100.00
        is_in_stock: 1
      website_ids:
        - 1 # website id
        - base # or website code
      category_ids:
        - 2                 # Default Category
      price: 12.99
      tax_class_id: 2       # Taxable Goods
      status: 1             # Enabled
      visibility: 4         # Visible in Catalog & Search
      /websites:            # Websites values where
           base:              # website code is the key for list of attributes in the website
              price: 10.99
      /stores:                # Store values, works in the same way as websites
           default:
              name: Book on Default Store
           english:
              name: Book on English Store
    # Second Product
    - entity_id: 2
      type_id: simple
      sku: another_book
      name: Another Book
      short_description: Another Book
      description: Another Book
      url_key: another_book
#.... An so on

As you see from the above example, eav is a new fixture type, catalog_product is an EAV entity type code. Each row represents a set of attribute values with special codes for multi-store values, /websites and /stores. If you want define a value for the attribute in a particular store or website you need place this value inside of /stores or /websites element by specifying store or website code with attribute code. For instance, if I want specify description attribute for german store, it will look like this:

# .. other attributes
/stores
  german:
    description: ---|
       Some description text in German
       Language that has a lot of
       lines....

Also you can extend EAV Fixture loader by adding custom loaders for entity types, you just need create a resource model and extend it from EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract, implement your custom process of load and add your resource model class alias into configuration:

<config>
    <phpunit>
          <suite>
               <fixture>
                    <[entity_type_code]>[your_module/model]</[entity_type_code]>
               </fixture>
          </suite>
    </phpunit>
</config>

By the way, with this configuration node you can override current loaders as well. If there is no custom loader specified, then ecomdev_phpunit/fixture_eav_default will be used.

For EAV entities, after loading of the fixture, can be run related indexers, so if you do not need any of them you should use @doNotIndex [index_code] and @doNotIndexAll annotation in your tests doc comment. It will speed up the tests evaluation process.

Websites, Store Groups, Stores…

Now you can setup any number of them for the test, because scope fixture type was added in this version. IT works pretty simple, just specify standard fields from that models:

scope:
  website: # Initialize websites for test
# Website One
    - website_id: 2
      code: my_test_website_one
      name: My Test Website One
      default_group_id: 2
# Website Two
    - website_id: 3
      code: my_test_website_two
      name: My Test Website Two
      default_group_id: 3
  group: # Initializes store groups
# Store Group 1
    - group_id: 2
      website_id: 2
      name: My Test Store Group One
      default_store_id: 2
      root_category_id: 2 # Default Category
# Store Group 2
    - group_id: 3
      website_id: 3
      name: My Test Store Group Two
      default_store_id: 3
      root_category_id: 2 # Default Category
  store: # Initializes store views
# Store 1
    - store_id: 2
      website_id: 2
      group_id: 2
      code: my_test_store_two
      name: My Test Store One
      is_active: 1
# Store 2
    - store_id: 3
      website_id: 3
      group_id: 3
      code: my_test_store_two
      name: My Test Store Two
      is_active: 1

Notice: Place fixture types data inside of the fixture file in the same order as it should be loaded. Because if you create website in the end of file, but will use its ID in at the beginning, you will get a fatal error.

Applying Store Scope

If you want to test behavior of your custom module for a particular store, then you can apply it by calling EcomDev_PHPUnit_Test_Case::setCurrentStore($store) method inside of your test.

/**
 * @test
 */
public myCheck()
{
    $this->setCurrentStore('english'); // By Store Code
    // ... some actions ...
    // Set another store
    $this->setCurrentStore(2); // By Store Id
    // ... etc
}

Improved Expectation Usage

Now it became more easy to use expectation for your test case if you have a lot data provider variables. Now you can load some part of fixture as Varien_Object and use it.
For instance, you have to test different products on different stores but via single logic. Expected results and input data of course is very various. So first of all you will create a data provider, that provides data for your test:

EcomDev/Example/Test/Product/providers/testName.yaml

- # First Data Set
  - 1
  - usa
- # Second Data Set
  - 1
  - canada
- # Third Data Set
  - 1
  - usa
# etc..

Then you will write a simple test:
EcomDev/Example/Test/Model/Product.php

class EcomDev_Example_Test_Model_Product extends EcomDev_PHPUnit_Test_Case
{
    /**
     * Example product Test
     * @test
     * @loadExpectation
     * @dataProvider dataProvider
     */
    public function priceCalculation($productId, $storeId)
    {
        $storeId = Mage::app()->getStore($storeId)->getId();
        /* @var $product Mage_Catalog_Model_Product */
        $product = Mage::getModel('catalog/product')
            ->setStoreId($storeId)
            ->load($productId);
        $expectations = $this->_getExpectations(
            '%s-%s', $productId, $storeId
        );
        // Your test itself
     }
}

Since you test depends on store id and product id, your expectation data maybe displayed in such a way.

And of course expectations:

1-2:  # Product 1 Store USA
  final_price: 9.99
  price: 12.99
1-3:  # Product 1 Store Canada
  final_price: 12.99
  price: 12.99
1-4:  # Product 1 Store Germany
  final_price: 5.99
  price: 9.99

If you noticed, now you can specify additional arguments to EcomDev_PHPUnit_Test_Case::_getExpectations() method. The first argument represents a format for sprintf, which will be used for formatting the other arguments. If only one argument is specified, then it will be used as constant value.

Small Example

In conclusion, would be great to give you some ideas how you can use it all together. Here is small test case for product, where is implemented test for multi-website price and price indexers functionality. Do not forget to create Example module, like explained in the previous article.

Test Case File

EcomDev/Example/Test/Model/Product.php

<?php
/**
 * Product price test case
 *
 */
class EcomDev_Example_Test_Model_Product extends EcomDev_PHPUnit_Test_Case
{
    /**
     * Product price calculation test
     *
     * @test
     * @loadFixture
     * @loadExpectation
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function priceCalculation($productId, $storeId)
    {
        $storeId = Mage::app()->getStore($storeId)->getId();
        /* @var $product Mage_Catalog_Model_Product */
        $product = Mage::getModel('catalog/product')
            ->setStoreId($storeId)
            ->load($productId);
        $expectations = $this->_getExpectations(
            '%s-%s', $productId, $storeId
        );
        // Check that final price
        // is the minimal one for the product
        $this->assertEquals(
            $expectations->getFinalPrice(),
            $product->getFinalPrice()
        );
        // Check that base price is proper value
        // for the current website
        $this->assertEquals(
            $expectations->getPrice(),
            $product->getPrice()
        );
    }
    /**
     * Test case for price index check
     *
     * @param int $storeId
     * @test
     * @dataProvider dataProvider
     * @loadFixture
     * @loadExpectation
     */
    public function priceIndex($storeId)
    {
        $this->setCurrentStore($storeId);
        /* @var $layer Mage_Catalog_Model_Layer */
        $layer = Mage::getModel('catalog/layer');
        $layer->setCurrentCategory(
            Mage::app()->getStore($storeId)->getRootCategoryId()
        );
        $expectations = $this->_getExpectations($storeId);
        $productCollection = $layer->getProductCollection();
        // Check that number of products the same as we expected
        $this->assertEquals(
            $expectations->getProductCount(),
            $productCollection->count()
        );
        foreach ($expectations->getItems() as $item) {
            $product = $productCollection->getItemById($item['id']);
            $this->assertInstanceOf('Mage_Catalog_Model_Product', $product);
            // Check that there minimal price the same as expected
            $this->assertEquals($item['minimal_price'], $product->getMinimalPrice());
            // Check that the base price the same as expected
            $this->assertEquals($item['price'], $product->getPrice());
        }
    }
}

Fixtures

EcomDev/Example/Test/Model/Product/fixtures/priceCalculation.yaml

scope:
  website: # Initialize websites
    - website_id: 2
      code: usa_website
      name: USA Website
      default_group_id: 2
    - website_id: 3
      code: canada_website
      name: Canada Website
      default_group_id: 3
    - website_id: 4
      code: german_website
      name: German Website
      default_group_id: 4
  group: # Initializes store groups
    - group_id: 2
      website_id: 2
      name: USA Store Group
      default_store_id: 2
      root_category_id: 2 # Default Category
    - group_id: 3
      website_id: 3
      name: Canada Store Group
      default_store_id: 3
      root_category_id: 2 # Default Category
    - group_id: 4
      website_id: 4
      name: German Store Group
      default_store_id: 4
      root_category_id: 2 # Default Category
  store: # Initializes store views
    - store_id: 2
      website_id: 2
      group_id: 2
      code: usa
      name: USA Store
      is_active: 1
    - store_id: 3
      website_id: 3
      group_id: 3
      code: canada
      name: Canada Store
      is_active: 1
    - store_id: 4
      website_id: 4
      group_id: 4
      code: germany
      name: Germany Store
      is_active: 1
config:
  default/catalog/price/scope: 1 # Set price scope to website
eav:
  catalog_product:
    - entity_id: 1
      type_id: simple
      sku: book
      name: Book
      short_description: Book
      description: Book
      url_key: book
      stock:
        qty: 100.00
        is_in_stock: 1
      website_ids:
        - usa_website
        - canada_website
        - german_website
      category_ids:
        - 2 # Default Category
      price: 12.99
      tax_class_id: 2 # Taxable Goods
      status: 1             # Enabled
      visibility: 4         # Visible in Catalog & Search
      /websites:            # Set different prices per website
        usa_website:
          special_price: 9.99
        german_website:
          price: 9.99
          special_price: 5.99

EcomDev/Example/Test/Model/Product/fixtures/priceIndex.yaml

scope:
  website: # Initialize websites
    - website_id: 2
      code: usa_website
      name: USA Website
      default_group_id: 2
    - website_id: 3
      code: canada_website
      name: Canada Website
      default_group_id: 3
    - website_id: 4
      code: german_website
      name: German Website
      default_group_id: 4
  group: # Initializes store groups
    - group_id: 2
      website_id: 2
      name: USA Store Group
      default_store_id: 2
      root_category_id: 2 # Default Category
    - group_id: 3
      website_id: 3
      name: Canada Store Group
      default_store_id: 3
      root_category_id: 2 # Default Category
    - group_id: 4
      website_id: 4
      name: German Store Group
      default_store_id: 4
      root_category_id: 2 # Default Category
  store: # Initializes store views
    - store_id: 2
      website_id: 2
      group_id: 2
      code: usa
      name: USA Store
      is_active: 1
    - store_id: 3
      website_id: 3
      group_id: 3
      code: canada
      name: Canada Store
      is_active: 1
    - store_id: 4
      website_id: 4
      group_id: 4
      code: germany
      name: Germany Store
      is_active: 1
config:
  default/catalog/price/scope: 1 # Set price scope to website
eav:
  catalog_product:
    - entity_id: 1
      type_id: simple
      sku: book
      name: Book
      short_description: Book
      description: Book
      url_key: book
      stock:
        qty: 100.00
        is_in_stock: 1
      website_ids:
        - usa_website
        - canada_website
        - german_website
      category_ids:
        - 2 # Default Category
      price: 12.99
      tax_class_id: 2 # Taxable Goods
      status: 1             # Enabled
      visibility: 4         # Visible in Catalog & Search
      /websites:            # Set different prices per website
        usa_website:
          special_price: 9.99
        german_website:
          price: 9.99
          special_price: 5.99
    - entity_id: 2
      type_id: simple
      sku: cd-case
      name: CD Case
      short_description: CD Case
      description: CD Case
      url_key: cd-case
      tier_price: # Yeah! This product has tier prices
        - qty: 3
          value: 2.99
        - qty: 5
          value: 2.88
        - website_id: 3 # Special tier price for Canadian people
          qty: 2
          value: 0.99
      stock:
        qty: 5.00
        is_in_stock: 1
      website_ids:
        - usa_website
        - canada_website
      category_ids:
        - 2 # Default Category
      price: 3.99
      tax_class_id: 2 # Taxable Goods
      status: 1             # Enabled
      visibility: 4         # Visible in Catalog & Search
      /websites:            # Set different prices per website
        usa_website:
          special_price: 2.99

Data Providers

EcomDev/Example/Test/Model/Product/providers/priceCalculation.yaml

-
  - 1
  - usa
-
  - 1
  - canada
-
  - 1
  - germany

EcomDev/Example/Test/Model/Product/providers/priceIndex.yaml

-
  - usa
-
  - canada
-
  - germany

And Finally Expectations

EcomDev/Example/Test/Model/Product/expectations/priceCalculation.yaml

1-2:  # Product=Book Store=USA
  final_price: 9.99
  price: 12.99
1-3:  # Product=Book Store=Canada
  final_price: 12.99
  price: 12.99
1-4:  # Product=Book Store=Germany
  final_price: 5.99
  price: 9.99

EcomDev/Example/Test/Model/Product/expectations/priceIndex.yaml

usa:  # Price Index Expectations for US Website
  product_count: 2
  items:
    - id: 1 # Book
      price: 12.99
      minimal_price: 9.99
    - id: 2 # CD case
      price: 3.99
      minimal_price: 2.88
canada:  # Price Index Expectations for Canadian Website
  product_count: 2
  items:
    - id: 1 # Book
      price: 12.99
      minimal_price: 12.99
    - id: 2 # CD case
      price: 3.99
      minimal_price: 0.99
germany:  # Price Index Expectations for German Website
  product_count: 1
  items:
    - id: 1 # Book
      price: 9.99
      minimal_price: 5.99

P.S.: Ongoing Versions

According to our public roadmap, in the next version you will be able to test controllers and layouts. So do not miss our future updates!

Ivan Chepurnyi
Magento guru / System architect
Ivan started as Magento Core developer in early 2007, since that time he already has 6 years of experience in different areas of Magento development. During all that time he developed enormous amount of modules and customizations, so now almost every dark corner in Magento functionality is investigated by him. He can’t keep that knowledge in secret, that why he's sharing it with the community and helps finding the way out of Magento complexity maze.

18 Responses to “New Version Of EcomDev_PHPUnit 0.1.2 Extension For Magento”

  1. Hello.

    The extension is pretty cool, but I have a small problem using it. I have a task which is related to modifying exact eav attribute table (adding new columns and special logic related to it). To test it I write a fixture like
    eav/attribute:
    – attribute_id: 999
    entity_type_id: 4
    attribute_code: xxx
    frontend_input: select
    my_new_added_field: yyy

    This works great, but the after finishing it truncates the eav_attribute table and I loose all the system attributes. So I cannot write a simple eav fixture for catalog_product like in your examples above, b/c name, status, visibility, etc do not exist anymore.
    Here I have a question to you: is there any workaround in your mind? I guess there is one – create all the required attributes in the fixture file before catalog_product, but I believe that there should be better solution.

    Thanks in advice.
    Daniel

    • Ivan Chepurnyi says:

      Hello Daniel,
      New attributes should be added via setup scripts of your module for now, but I will think concerning it, it shouldn’t be a problem.

    • Ivan Chepurnyi says:

      By the way, I am not sure that attributes should be added via fixtures, since it is the eav metadata, that should be managed via install scripts of your module. It is like adding a table column via fixture.

      • Yes, I agree that attributes should be added via installers, but imagine the following situation:
        I need to add new column to all eav attributes (like eav attribute’s attribute). And then I need to write special logic, let’s say depending on this column and attribute frontend input type. My module does not add any new attributes, they will be added later in the admin by store administrators, and they also will set the value to my column. But I need to test the new logic. My column might have different values and this should be tested, so I need to write fixtures for attributes with different values of new column.
        See what I mean?

        • Ivan Chepurnyi says:

          Hello David,
          so you need a mechanism for updating a particular value in attribute instance? right? Then it is easy to solve by updating value in setUp method and revert it back in tearDown, without database modifications, since Mage::getSingleton(‘eav/config’) caches attribute objects in memory. You can do it in your test case or create new fixture type by extending EcomDev_PHPUnit_Model_Fixture. Also if you want this new feature in the next releases you can submit a feature request.

          As for custom attributes via fixtures, I will think over this idea, but it will be too bad for unit tests performance, because it will require reinitialization of entity type singleton for each test.

          And what about about using of mock objects for your custom logic testing? Is it not fit your requirements?

          • Hello Ivan.

            Yes, your idea about in-memory attribute should work, thanks for the advice.

            Also I have an idea for the future release which may help to solve such issues – maybe there should be an ability to set an annotation to the test case which will tell the framework that it should not truncate fixtures tables, but revert to the initial state. Yes, this will be slower, but for one or two specific test cases this might be useful. I’ll also post it to you issue tracker.

            Thanks

  2. Hi Ivan,
    Is it possible to test observers with your extension?
    I have several doubts about this point:
    1. how to check that it is triggered? Could I dispatch the event I’m listening to from within my test case?
    2. how can i pass the $observer variable to another methods, in another class? Can I setup a fixture?
    Thanks in advance :)

    • Hi David,

      1. You should invoke your observer method in the test case. For config.xml files configuration I am planing to create assertion methods, like $this->assertEventIsHandled(‘area’, ‘eventName’, ‘module/observer’, ‘methodName’) for testing of setting events properly, finally I have some time to improve the extension, had a lot of project work last month…

      2. Actually I think it will not be a problem for you to create an observer instance, but I will add some kind of factory method for observer object creation from passed event data.

      • Hi Ivan, thanx for answering.
        I’ll try this, and I’m looking forward for the next release of the extension.
        cheers

  3. Hello Ivan,

    Thanks for such an awesome Magento module! I’ve got the question about mocking objects with you module. My project uses a lot of externals API’s with custom modules and I would like to mock some of them when running tests. Do you have any idea or some kind of author’s vision how can that be done?

    As I figured out you are replacing the config model with your own via Reflection. Maybe that config model could be used to return the mocks instead of the real class names when instantinating the class via Mage::getModel or with Mage::helper? Also I was thinking about defining what should be mocked on module basis. I mean I don’t need mocking of API when testing the module itself, but I would gladly mock it when testing some other module.

    What do you think? I would gladly elaborate the feature just to be sure I’m doing it correctly so that I could be merged into the upstream.

    • Hi Paul,

      Ok, maybe I can create some sort of method that will temporary replace calls to Mage::getConfig()->getModelInstance() and other factory methods as well. Please submit it as feature request to our issue tracking.

  4. Sergey A. Lisenko says:

    Hi, Ivan.
    Could you explain how test adminhtml controllers?
    I’ve tried, but seems Magento expects an authenticated user.
    Anyway,

    $this->dispatch('mymodule/adminhtml_controller/someaction/');
    

    returns a php-error “Call to a member function getUsername() on a non-object in …/app/design/adminhtml/default/default/template/page/header.phtml”
    Thanks you!

  5. I was curious if you could give an example of how to use a fixture to test an order? Make sure the order has the correct shipping/billing address along with the correct items. I’m running into problems when I use a fixture to load up order data.

    Thanks,
    Joshua

  6. Is there anyway to define grouped products in the fixture? Very specifically – I would like to define the simple products under the grouped and the price they’re supposed to return.

  7. Hi Ivan, thanks as always for this. I’m keen to develop a unit test that can verify the adminhtml ACLs for a module match the adminhtml menu and actions. Can you suggest the best way to use EcomDev_PHPUnit_Test_Case_Config or other assertions to test this? Thanks, Jonathan

    • Hi Jonathan,

      Thanks for nice feedback. I think it is nice addition for an extension, but I don’t have time now for creating a new constraint. But you can easily make it yourself and make a pull request. The class you need to extend from is EcomDev_PHPUnit_Constraint_Config_Abstract and inside of EcomDev_PHPUnit_Test_Case_Config you need to use not Mage::getConfig() as actual value for check, but you should apply constraint to Mage::getSingleton(‘adminhtml/config’).

  8. dfgd dfgdfg says:

    Hi,
    this is a very dumb question, but after all this, how do i “run” the test? do i just run command “phpunit Product.php” ? In which case I get error
    PHP Fatal error: Class ‘EcomDev_PHPUnit_Test_Case’ not found in /var/www/app/code/community/EcomDev/Example/Test/Model/Product.php on line 7

Trackbacks/Pingbacks

  1. Tweets that mention New Version Of EcomDev_PHPUnit 0.1.2 Extension For Magento | E-commerce developers blog -- Topsy.com - [...] This post was mentioned on Twitter by magefeed and Eсommerce Developers, Ivan Chepurnyi. Ivan Chepurnyi said: Fixtures for EAV ...
  2. Links 06/2011: Magento, xt:Commerce VEYTON, Mobile & mehr | Matthias Zeis - [...] interessant war, doch die Slides lassen es vermuten.Die E-commerce developers haben ihre Unit-Testsuite-Erweiterung für Magento aktualisiert. Wichtigste Neuerung ist ...
  3. Magento – UnitTests – Mock Objects(Resolved) - Tech Forum Network - [...] am writing some tests for a Magento module, using Ivan Chepurnyi’s extension, and I’m having trouble using the mock ...

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>