The latest iteration of my "Writing testable code" presentation from Meet-Magento Romaina 2016 in Cluj-Napoca.
It covers basics on what properties of production code make testing simpler.
3. You know basic PHPUnit.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
4. You want
→ Confidence in deploys
→ Experience joy when writing tests
→ Have fun doing code maintaince
→ Get more $$$ out of testing
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
5. In short, you want
→ Testable code
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
6. When is code "testable"?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
7. When testing
is simple & easy.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
8. What makes a
test simple?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
9. It is simple to write.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
10. It is easy to read.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
11. What does
"easy to read"
mean?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
12. It's intent is clear.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
13. The test is short.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
19. It are properties of the
production code
that make testing
easy or hard.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
20. What does easy to test
code look like?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
22. <?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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
23. Are we going to write
Unit Tests?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
25. Unit tests only provide
value for new code.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
26. For previously untested code,
Integration Tests
are much more valuable.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
27. What would make it
simpler to test?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
28. If the class where
smallerit would be simpler to test.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
29. First attempt:
Splitting the class based on purpose.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
30. What does the
class do?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
31. 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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
32. Lets split it into
Netzarbeiter_CustomerActivation_Model...
..._Observer_ProhibitInactiveLogins
..._Observer_EmailNotifications
..._Observer_AdminhtmlCustomerGrid
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
33. <?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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
34. <?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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
35. <?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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
36. Is this simpler
to test?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
37. Only minor difference in
testing effort.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
44. Almost all of them are core classes.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
45. Only two classes are part of the module:
Netzarbeiter_CustomerActivation_Model_Observer
Netzarbeiter_CustomerActivation_Helper_Data
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
46. Based on the names,
why do these classes exist?
Netzarbeiter_CustomerActivation_Model_Observer
Netzarbeiter_CustomerActivation_Helper_Data
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
47. The names don't
tell us anything.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
48. Extract parts by giving them
meaningful names
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
49. But where to start?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
50. Separate business logic
from entry points.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
51. Entry points are the places Magento provides for
our custom code.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
52. Entry points:
→ Observers
→ Plugins
→ Controllers
→ Cron Jobs
→ Preferences
→ Console Commands
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
53. Entry points link
Business logic
!
Magento
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
55. What are the benefits?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
56. For testing:
The custom code can be triggered
independently of the entry point.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
57. In our example,
what is the
entry point?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
60. 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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
61. New Code, without business logic:
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
62. public function customerLogin(Event $event)
{
if (! $this->isModuleActive()) {
return;
}
$this->getCustomerLoginSentry()->abortLoginIfNotActive(
$event->getData('customer')
);
}
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
63. And this class is
simpler to test?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
64. Yes, as there is
much less logic.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
65. Most of the logic is delegated to collaborators.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
72. DI is a Magento 2 thing,
right?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
73. DI can be everywhere!
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
74. Injecting Test Doubles
in Magento 1
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
75. 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;
// ...
}
Problem: It muddies intention revealing class interfaces.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
76. Constructor Injection
public function testDelegatesToLoginSentry()
{
$mockLoginSentry = $this->createMock(LoginSentry::class);
$mockLoginSentry->expects($this->once())
->method('abortLoginIfNotActive');
$observer = new Netzarbeiter_CustomerActivation_Model_Observer(
$mockLoginSentry
);
// ...
}
Problem: Standard Magento 1 instantiation can't do it.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
77. Ugly but hey it works.
/**
* @param LoginSentry $loginSentry
*/
public function __construct($loginSentry = null)
{
$this->loginSentry = $loginSentry;
}
// ...
private function getCustomerLoginSentry()
{
return $this->loginSentry ?? Mage::getModel(self::$sentry);
}
Paradoxical: Optional Dependency Injection..? o_O
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
78. Injected collaborators
make for simple tests!
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
79. Delegation allow us to create
classes with a specific purpose.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
80. We can give descriptive names.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
81. Model with one responsibility:
class Netzarbeiter_CustomerActivation_Model_CustomerLoginSentry
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
82. 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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
83. This business logic is now
independent of the entry point.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
84. It can be called from anywhere:
→ Test
→ Observer
→ Controller
→ Model Rewrite
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
85. Back to the example code...
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
86. One other subtle thing here 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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
87. No magic method __call() calls!
// Old "magic" code:
$event->getCustomer();
// New code:
$event->getData('customer')
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
88. Why does that
improve testability?
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
89. Creating a mock with
magic methods is ugly!
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
90. Lots of setup code in tests
distracts
from the important parts.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
91. Noisy test double creation:
$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 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
92. Much nicer:
$mockEvent = $this->createMock(Event::class);
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
102. → Adherence to the Law of Demeter.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
103. → Separation of methods that causes
side effects from
methods returning values.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
104. → Avoidance of method call chaining.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
105. → Methods having a single level of detail.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp
106. Lets keep these for another time.
Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp