What to mock in a Magento 2 unit test

You write unit tests for a Magento 2 module and need to mock a core class, like the store model. This class has a few dependencies which have more dependencies and it’s getting complicated to mock. Although PHPUnit 5.4 has a createMock() method that automatically stubs methods and let them return new stubs if they have a return type.

Or: you mock a service contract like getList() in the product repository, which takes a search criteria instance as parameter, which must be returned by a search criteria factory. Then it returns a product search result, which has a getItems() method, which returns a array of products. So you end up mocking countless dependendencies for a single method call

Do these situations sound familiar? It does not need to be like that, though! Let me show you a sane way to deal with Magento dependencies.

Continue reading “What to mock in a Magento 2 unit test”

Learn Refactoring to Framework Independent Code

I’m proud to annouce that I will present the Nomad Mage session of January 2017. Nomad Mage is the Magento offspring of Nomad PHP and describes itself as:

Nomad Mage® is a virtual user group for Magento developers who understand that they need to keep learning to grow professionally. We meet online monthly to hear some of the best speakers in the community share what they’ve learned.

My topic: “Keep Magento Out of Your Magento Extensions – Refactoring to Framework Independent Code”

To port an existing Magento 1 extension to Magento 2, it can be helpful to first extract the business logic into a reusable library. This also makes for more testable and future-proof code. But how can it be done?

On Nomad Mage I’m going to walk through real examples to show you how such a refactoring can be approached. Although we will focus on refactoring existing Magento 1 extensions, the patterns you will learn are also useful for new extensions, Magento 1 or Magento 2.

Continue reading “Learn Refactoring to Framework Independent Code”

Column not found: 1054 Unknown column ‘sales_bestsellers_aggregated_yearly.product_type_id’ in ‘field list

Column not found: 1054 Unknown column ‘sales_bestsellers_aggregated_yearly.product_type_id’ in ‘field list

You might receive this error message in a fresh Magento 1.9.3 installation with sample data when trying to log into the admin panel. I got it after installing https://github.com/andreaskoch/dockerized-magento and found a few reports online, but no solution.

I don’t know the root cause yet, but to be able to log in again, you can set a different admin startup page:

n98-magerun config:set admin/startup/page system/config
n98-magerun cache:clear config

If you don’t have magerun installed (you should!), change the value in the core_config_data table instead and delete the cache.

Now delete the adminhtml cookie in your browser and you can log in again. Just don’t try to open the dashboard.


Full Stack Trace

a:5:{i:0;s:698:"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sales_bestsellers_aggregated_yearly.product_type_id' in 'field list', query was: SELECT COUNT(*) FROM (SELECT MAX(DATE_FORMAT(period, '%Y-%m-%d')) AS `period`, SUM(qty_ordered) AS `qty_ordered`, `sales_bestsellers_aggregated_yearly`.`product_id`, MAX(product_name) AS `product_name`, MAX(product_price) AS `product_price`, `sales_bestsellers_aggregated_yearly`.`product_type_id` FROM `sales_bestsellers_aggregated_yearly` WHERE (EXISTS (SELECT 1 FROM `catalog_product_entity` AS `existed_products` WHERE (sales_bestsellers_aggregated_yearly.product_id = existed_products.entity_id))) AND (store_id IN(0)) GROUP BY `product_id` LIMIT 5) AS `t`";i:1;s:4693:"#0 /var/www/html/web/lib/Varien/Db/Statement/Pdo/Mysql.php(110): Zend_Db_Statement_Pdo->_execute(Array)
#1 /var/www/html/web/app/code/core/Zend/Db/Statement.php(291): Varien_Db_Statement_Pdo_Mysql->_execute(Array)
#2 /var/www/html/web/lib/Zend/Db/Adapter/Abstract.php(480): Zend_Db_Statement->execute(Array)
#3 /var/www/html/web/lib/Zend/Db/Adapter/Pdo/Abstract.php(238): Zend_Db_Adapter_Abstract->query('SELECT COUNT(*)...', Array)
#4 /var/www/html/web/lib/Varien/Db/Adapter/Pdo/Mysql.php(504): Zend_Db_Adapter_Pdo_Abstract->query('SELECT COUNT(*)...', Array)
#5 /var/www/html/web/lib/Zend/Db/Adapter/Abstract.php(828): Varien_Db_Adapter_Pdo_Mysql->query(Object(Varien_Db_Select), Array)
#6 /var/www/html/web/lib/Varien/Data/Collection/Db.php(225): Zend_Db_Adapter_Abstract->fetchOne(Object(Varien_Db_Select), Array)
#7 /var/www/html/web/lib/Varien/Data/Collection.php(225): Varien_Data_Collection_Db->getSize()
#8 /var/www/html/web/lib/Varien/Data/Collection.php(211): Varien_Data_Collection->getLastPageNumber()
#9 /var/www/html/web/lib/Varien/Data/Collection/Db.php(522): Varien_Data_Collection->getCurPage()
#10 /var/www/html/web/lib/Varien/Data/Collection/Db.php(569): Varien_Data_Collection_Db->_renderLimit()
#11 /var/www/html/web/app/code/core/Mage/Reports/Model/Resource/Report/Collection/Abstract.php(285): Varien_Data_Collection_Db->load(false, false)
#12 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php(550): Mage_Reports_Model_Resource_Report_Collection_Abstract->load()
#13 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Dashboard/Tab/Products/Ordered.php(66): Mage_Adminhtml_Block_Widget_Grid->_prepareCollection()
#14 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php(643): Mage_Adminhtml_Block_Dashboard_Tab_Products_Ordered->_prepareCollection()
#15 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php(649): Mage_Adminhtml_Block_Widget_Grid->_prepareGrid()
#16 /var/www/html/web/app/code/core/Mage/Core/Block/Abstract.php(922): Mage_Adminhtml_Block_Widget_Grid->_beforeToHtml()
#17 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Dashboard/Grids.php(64): Mage_Core_Block_Abstract->toHtml()
#18 /var/www/html/web/app/code/core/Mage/Core/Block/Abstract.php(297): Mage_Adminhtml_Block_Dashboard_Grids->_prepareLayout()
#19 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(456): Mage_Core_Block_Abstract->setLayout(Object(Mage_Core_Model_Layout))
#20 /var/www/html/web/app/code/core/Mage/Adminhtml/Block/Dashboard.php(75): Mage_Core_Model_Layout->createBlock('adminhtml/dashb...')
#21 /var/www/html/web/app/code/core/Mage/Core/Block/Abstract.php(297): Mage_Adminhtml_Block_Dashboard->_prepareLayout()
#22 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(456): Mage_Core_Block_Abstract->setLayout(Object(Mage_Core_Model_Layout))
#23 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(472): Mage_Core_Model_Layout->createBlock('adminhtml/dashb...', 'dashboard')
#24 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(239): Mage_Core_Model_Layout->addBlock('adminhtml/dashb...', 'dashboard')
#25 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(205): Mage_Core_Model_Layout->_generateBlock(Object(Mage_Core_Model_Layout_Element), Object(Mage_Core_Model_Layout_Element))
#26 /var/www/html/web/app/code/core/Mage/Core/Model/Layout.php(210): Mage_Core_Model_Layout->generateBlocks(Object(Mage_Core_Model_Layout_Element))
#27 /var/www/html/web/app/code/core/Mage/Core/Controller/Varien/Action.php(344): Mage_Core_Model_Layout->generateBlocks()
#28 /var/www/html/web/app/code/core/Mage/Core/Controller/Varien/Action.php(269): Mage_Core_Controller_Varien_Action->generateLayoutBlocks()
#29 /var/www/html/web/app/code/core/Mage/Adminhtml/Controller/Action.php(275): Mage_Core_Controller_Varien_Action->loadLayout(NULL, true, true)
#30 /var/www/html/web/app/code/core/Mage/Adminhtml/controllers/DashboardController.php(40): Mage_Adminhtml_Controller_Action->loadLayout()
#31 /var/www/html/web/app/code/core/Mage/Core/Controller/Varien/Action.php(418): Mage_Adminhtml_DashboardController->indexAction()
#32 /var/www/html/web/app/code/core/Mage/Core/Controller/Varien/Router/Standard.php(254): Mage_Core_Controller_Varien_Action->dispatch('index')
#33 /var/www/html/web/app/code/core/Mage/Core/Controller/Varien/Front.php(172): Mage_Core_Controller_Varien_Router_Standard->match(Object(Mage_Core_Controller_Request_Http))
#34 /var/www/html/web/app/code/core/Mage/Core/Model/App.php(365): Mage_Core_Controller_Varien_Front->dispatch()
#35 /var/www/html/web/app/Mage.php(692): Mage_Core_Model_App->run(Array)
#36 /var/www/html/web/index.php(83): Mage::run('', 'store')
#37 {main}";s:3:"url";s:70:"/index.php/admin/dashboard/index/key/8565326cb6d7ae31c17ba1e04f2631bc/";s:11:"script_name";s:10:"/index.php";s:4:"skin";s:5:"admin";}

PDF Generation in Magento 2

magento-pdf-layout

The Magento core methods to generate PDF files are rather unflexible. An alternative are tools that convert HTML to PDF.

In our current Magento 2 project, which we (integer_net) develop together with Stämpfli AG, there is a requirement to dynamically create a PDF catalog based on selected products, which has almost the same layout as the product lists in the shop. So, generating this PDF based on HTML suggested itself.

In this article I present our solution, which integrates wkhtmltopdf with the Magento layout. At the end you will find a link to the base module on Github.

Continue reading “PDF Generation in Magento 2”

Isolating Domain Logic in Magento Customizations

Lately I’ve been advocating decoupling business logic from the framework (i.e. Magento) a lot.

This has multiple advantages:

  • Benefit from test driven development (TDD) without having to mock a bunch of core classes.
  • Possible reuse in different applications (for example Magento 1 and Magento 2)
  • Having separated bounded contexts helps to view parts of the domain isolated and without distraction.

Even in chirurgic modifications that we often have to do in Magento projects, it is worth identifying the actual logic and extracting it from the actual Magento classes.

Let me demonstrate it with a real-world example:

Read more at integer-net.com

Stop using Helpers

“Helpers” are often used as convenient collection of functions. They are also a sign of bad design, and I want you to stop writing them. I’ll quote myself

In general, having classes named “Helper”, “Util” or similar just says “I have some functions that I don’t know where to put” and don’t make much sense as a class.

It’s not very object oriented. Not at all to be frank. The idea of object oriented programming is that there are objects that send each others messages. They have an active role in the system and are not just containers for data and code, which would be a very procedural way to see them.

So what would be the role of a helper? A butler maybe, that does not act on its own and will do anything you tell him. But to do that, he needs access to your whole household, your bank account and your car.

Continue reading “Stop using Helpers”

Magento 2 Integration Tests: @magentoConfigFixture

I did not find a good documentation on how to use the @magentoConfigFixture annotation for configuration fixtures in Magento 2 integration tests, so here is my summary after inspecting the core code (Magento 2.1.0, Magento\TestFramework\Annotation\ConfigFixture)

How to use @magentoConfigFixture

Set default value 42 for configuration path x/y/z:

/**
 * @magentoConfigFixture default/x/y/z 42
 */

Set store specific value 42 for configuration path x/y/z in store with code store1

/**
 * @magentoConfigFixture store1_store x/y/z 42
 */

Set store specific value 42 for configuration path x/y/z in current store (i.e. default store)

/**
 * @magentoConfigFixture current_store x/y/z 42
 */

These are all possible formats. The first parameter must end with _store or be ommitted. And if it is omitted, the path must start with default/, otherwise it is ignored.

Implications

  • You cannot set configuration values on website level
  • Do not use “current” as a real store code, otherwise you cannot use config fixtures for that store

EcomDev PHPUnit Tip #14

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 #14: Registry Fixtures

As already explained in Tip #1, helpers, singletons and registry values can be reset per test. Finding the problematic singletons etc. often was the most difficult part of writing integration tests with EcomDev, so I started to collect them centralized in one fixtures/registry.yaml file for all tests 1. Better reset one too many than one to little.

The file is structured like this:
Continue reading “EcomDev PHPUnit Tip #14”

5 Minute Tips: Magento Performance Tweaks

My “Week On StackExchange” Series takes a break at the moment because I don’t have not too much blog-worthy.

Instead, I’m starting something new again: Tips on a specific topic which you can read in 5 minutes maximum during coffee break. Mostly not by me, just found by me 🙂

I don’t try to make it a regular thing but I have a loose collection of useful stuff so why not use the blog to bring order into it and hopefully it’s also useful for others.

Let’s start with Magento Performance Tweaks, all with little effort and can be used without hesitation:

Continue reading “5 Minute Tips: Magento Performance Tweaks”