The document discusses writing testable code in Magento 1 and 2. It begins by describing assumptions and goals, such as having confidence in deployments and enjoying writing tests. It then discusses what makes code testable, including keeping classes focused on a single purpose/responsibility. The document uses an example Magento module to demonstrate refactoring code from a monolithic observer class to split it into smaller classes separated by purpose. This makes the code easier to test by isolating business logic from entry points like observers. The document advocates extracting business logic from entry points and delegating it to collaborator classes to simplify testing.
3. You know basic PHPUnit.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
4. You want
→ Confidence in deploys
→ Experience joy when writing tests
→ Have fun doing code maintaince
→ Get more $$$ from testing
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
5. In short, you want
→ Testable code
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
19. It are properties of the
production code
that make testing
easy or hard.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
20. "It's no big thing,
but you make big things
out of little things sometimes."
~~ Robert Duvall
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
21. What does easy to
test code look like?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
22. A bad case of legacy:
Event Observer
(for Magento 1)
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
23. <?php
use Varien_Event_Observer as Event;
class Netzarbeiter_CustomerActivation_Model_Observer
{
// Check if the customer has been activated, if not, throw login error
public function customerLogin(Event $event) {...}
// Flag new accounts as such
public function customerSaveBefore(Event $event) {...}
// Send out emails
public function customerSaveAfter(Event $event) {...}
// Abort registration during checkout if default activation status is false
public function salesConvertQuoteAddressToOrder(Event $event) {...}
// Add customer activation option to the mass action block
public function adminhtmlBlockHtmlBefore(Event $event) {...}
// Add the customer_activated attribute to the customer grid collection
public function eavCollectionAbstractLoadBefore(Event $event) {...}
// Add customer_activated column to CSV and XML exports
public function coreBlockAbstractPrepareLayoutAfter(Event $event) {...}
// Remove the customer id from the customer/session, in effect causing a logout
public function actionPostdispatchCustomerAccountResetPasswordPost(Event $event)
{...}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
24. Are we going to
write Unit Tests?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
25. For > 500 lines of legacy?
Hell NO!
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
26. What would make it
simpler to test?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
27. If the class where
smallerit would be simpler to test.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
28. First attempt:
Splitting the class based on
purpose.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
29. What does the
class do?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
30. 1. Prevents inactive customer logins.
2. Sends notification emails.
3. Adds a column to the customer grid.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
31. Lets split it into
Netzarbeiter_CustomerActivation_Model...
..._Observer_ProhibitInactiveLogins
..._Observer_EmailNotifications
..._Observer_AdminhtmlCustomerGrid
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
32. <?php
use Varien_Event_Observer as Event;
class Netzarbeiter_CustomerActivation_Model_Observer_ProhibitInactiveLogin
{
// Check if the customer has been activated, if not, throw login error
public function customerLogin(Event $event)
{...}
// Abort registration during checkout if default activation status is false
public function salesConvertQuoteAddressToOrder(Event $event)
{...}
// Remove the customer ID from the customer/session causing a logout
public function actionPostdispatchCustomerAccountResetPasswordPost(Event $event)
{...}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
33. <?php
use Varien_Event_Observer as Event;
class Netzarbeiter_CustomerActivation_Model_Observer_EmailNotifications
{
// Flag new accounts as such
public function customerSaveBefore(Event $event)
{...}
// Send out emails
public function customerSaveAfter(Event $event)
{...}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
34. <?php
use Varien_Event_Observer as Event;
class Netzarbeiter_CustomerActivation_Model_Observer_AdminhtmlCustomerGrid
{
// Add customer activation option to the mass action block
public function adminhtmlBlockHtmlBefore(Event $event)
{...}
// Add the customer_activated attribute to the customer grid collection
public function eavCollectionAbstractLoadBefore(Event $event)
{...}
// Add customer_activated column to CSV and XML exports
public function coreBlockAbstractPrepareLayoutAfter(Event $event)
{...}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
35. Is this simpler
to test?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
36. Only minor difference in
testing effort.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
43. Almost all of them are core classes.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
44. Only two classes are part of the module:
Netzarbeiter_CustomerActivation_Model_Observer
Netzarbeiter_CustomerActivation_Helper_Data
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
45. Based on the names,
why do they exist?
Netzarbeiter_CustomerActivation_Model_Observer
Netzarbeiter_CustomerActivation_Helper_Data
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
46. The names don't
tell us anything.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
47. Extract parts by giving them
meaningful names
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
48. But where to start?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
49. Separate business logic
from the entry points.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
50. Entry points are the places Magento
provides us to put our custom code.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
51. Entry points:
→ Observers
→ Plugins
→ Controllers
→ Cron Jobs
→ Preferences
→ Console Commands
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
52. Entry points link
Business logic
!
Magento
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
55. For testing:
The custom code can be triggered
independently of the entry point.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
56. In or example,
what is the
entry point?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
59. public function customerLogin($observer)
{
$helper = Mage::helper('customeractivation');
if (!$helper->isModuleActive()) {
return;
}
if ($this->_isApiRequest()) {
return;
}
$customer = $observer->getEvent()->getCustomer();
$session = Mage::getSingleton('customer/session');
if (!$customer->getCustomerActivated()) {
$session->setCustomer(Mage::getModel('customer/customer'))
->setId(null)
->setCustomerGroupId(Mage_Customer_Model_Group::NOT_LOGGED_IN_ID);
if ($this->_checkRequestRoute('customer', 'account', 'createpost')) {
$message = $helper->__('Please wait for your account to be activated');
$session->addSuccess($message);
} else {
Mage::throwException($helper->__('This account is not activated.'));
}
}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
60. New Code, without business logic:
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
61. public function customerLogin(Event $event)
{
if (! $this->isModuleActive()) {
return;
}
$this->getCustomerLoginSentry()->abortLoginIfNotActive(
$event->getData('customer')
);
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
62. And this class is
simpler to test?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
63. Yes, as there is
much less logic.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
64. Most of the logic is delegated to
collaborators.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
71. DI is a Magento 2
thing, right?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
72. DI can be everywhere!
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
73. Injecting Test Doubles
in Magento 1
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
74. Setter Injection
public function testDelegatesToLoginSentry()
{
$mockLoginSentry = $this->createMock(LoginSentry::class);
$mockLoginSentry->expects($this->once())
->method('abortLoginIfNotActive');
$observer = new Netzarbeiter_CustomerActivation_Model_Observer();
$observer->loginSentry = $mockLoginSentry;
// ...
}
Problematic because it makes the class interface less clean.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
75. Constructor Injection
public function testDelegatesToLoginSentry()
{
$mockLoginSentry = $this->createMock(LoginSentry::class);
$mockLoginSentry->expects($this->once())
->method('abortLoginIfNotActive');
$observer = new Netzarbeiter_CustomerActivation_Model_Observer(
$mockLoginSentry
);
// ...
}
Problematic because standard Magento 1 instantiation.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
76. Ugly but works fine.
/**
* @param LoginSentry $loginSentry
*/
public function __construct($loginSentry = null)
{
$this->loginSentry = $loginSentry;
}
// ...
private function getCustomerLoginSentry()
{
return isset($this->loginSentry) ?? Mage::getModel(self::$sentry);
}
Optional Dependencies?! (WTF LOL)
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
77. Injected collaborators
make for simple tests!
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
78. Delegation allow us to create
classes with a specific purpose.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
79. We can give descriptive names.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
80. Model with specific responsibility
class Netzarbeiter_CustomerActivation_Model_CustomerLoginSentry
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
81. public function abortLoginIfNotActive(
Mage_Customer_Model_Customer $customer
) {
if (! $customer->getData('customer_activated') {
$this->getSession()->logout();
$this->getDisplay()->showLoginAbortedMessage();
}
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
82. This business logic is now
independent of the entry point
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
83. It can be called from anywhere
→ Observer
→ Controller
→ Model Rewrite
→ Test
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
84. Back to the example code...
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
85. There is one other thing here that makes testing easier:
public function customerLogin(Event $event)
{
if (! $this->isModuleActive()) {
return;
}
$this->getCustomerLoginSentry()->abortLoginIfNotActive(
$event->getData('customer')
);
}
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
86. No magic method call.
// Old code:
$event->getCustomer();
// New code:
$event->getData('customer')
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
87. Why does that
improve testability?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
88. Creating a mock with
magic methods is ugly!
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
89. Noisy code in test is distracting.
$methods = array_merge(
get_class_methods(Event::class),
['getCustomer']
);
$mockEvent = $this->getMockBuilder(Event::class)
->setMethods($methods)
->getMock();
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
90. Much simpler:
$mockEvent = $this->createMock(Event::class);
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
92. What makes code
simple to test?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
93. → Separation of
Business Logic
and
Entry Points
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
94. → Small classes
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
95. → Encapsulation of Business Logic in
specific classes
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
96. → Delegation to
injectable dependencies
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
97. → Real methods
over
magic methods
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
98. Is there more?
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
99. → Law of Demeter.
→ Separation of code that causes side
effects from code that return a value.
→ No method call chaining.
→ Single level of detail within methods.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
100. Lets keep these for another time.
Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp