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/IvanChepurnyi/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!
18 Responses to “New Version Of EcomDev_PHPUnit 0.1.2 Extension For Magento”
Trackbacks/Pingbacks
- 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 ...
- 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 ...
- 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 ...










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
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.
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?
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
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
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.
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!
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
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.
@James, as for grouped products, you need to add records to product link tables with simple – grouped identifiers.
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’).
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