Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Writing Testable Code (for Magento 1 and 2)

1,253 views

Published on

What makes writing tests easy and fun?
These are the slides for the 25 minutes presentation at Meet-Magento PL on 19. September 2016.

Published in: Software
  • Be the first to comment

Writing Testable Code (for Magento 1 and 2)

  1. 1. Writing Testable Code (for Magento 1 and 2) Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  2. 2. Assumptions Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  3. 3. You know basic PHPUnit. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  4. 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. 5. In short, you want → Testable code Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  6. 6. When is code "testable"? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  7. 7. When testing is simple & easy. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  8. 8. What makes a test simple? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  9. 9. It is simple to write. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  10. 10. It is easy to read. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  11. 11. What does "easy to read" mean? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  12. 12. It's intent is clear. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  13. 13. The test is short. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  14. 14. Good names. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  15. 15. It only does one thing. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  16. 16. Clean Code is for Production Code & Test Code Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  17. 17. What does "simple to write" mean? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  18. 18. Test code depends on production code Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  19. 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. 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. 21. What does easy to test code look like? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  22. 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. 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. 24. Are we going to write Unit Tests? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  25. 25. For > 500 lines of legacy? Hell NO! Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  26. 26. What would make it simpler to test? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  27. 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. 28. First attempt: Splitting the class based on purpose. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  29. 29. What does the class do? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  30. 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. 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. 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. 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. 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. 35. Is this simpler to test? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  36. 36. Only minor difference in testing effort. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  37. 37. Why? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  38. 38. The same tests as before. Only split into three classes. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  39. 39. Second attempt: Lets go beyond superficial changes. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  40. 40. Lets look at the design. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  41. 41. What collaborators are used? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  42. 42. Collaborators: Netzarbeiter_CustomerActivation_Helper_Data Mage_Customer_Model_Customer Mage_Customer_Model_Session Mage_Customer_Model_Group Mage_Customer_Helper_Address Mage_Customer_Model_Resource_Customer_Collection Mage_Core_Controller_Request_Http Mage_Core_Controller_Response_Http Mage_Core_Exception Mage_Core_Model_Session Mage_Core_Model_Store Mage_Sales_Model_Quote_Address Mage_Sales_Model_Quote Mage_Eav_Model_Config Mage_Eav_Model_Entity_Type Mage_Adminhtml_Block_Widget_Grid_Massaction Mage_Adminhtml_Block_Widget_Grid Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  43. 43. Almost all of them are core classes. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  44. 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. 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. 46. The names don't tell us anything. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  47. 47. Extract parts by giving them meaningful names Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  48. 48. But where to start? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  49. 49. Separate business logic from the entry points. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  50. 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. 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. 52. Entry points link Business logic ! Magento Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  53. 53. Remove all Business Logic from Entry Points. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  54. 54. What are the benefits? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  55. 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. 56. In or example, what is the entry point? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  57. 57. Observer Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  58. 58. Old Observer Code: Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  59. 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. 60. New Code, without business logic: Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  61. 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. 62. And this class is simpler to test? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  63. 63. Yes, as there is much less logic. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  64. 64. Most of the logic is delegated to collaborators. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  65. 65. $this->getCustomerLoginSentry()->abortLoginIfNotActive( $event->getData('customer') ); Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  66. 66. How does the delegation look in detail? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  67. 67. private static $sentryClass = 'customeractivation/customerLoginSentry'; /** * @return Netzarbeiter_CustomerActivation_Model_CustomerLoginSentry */ private function getCustomerLoginSentry() { return $this->loginSentry ?? Mage::getModel(self::$sentryClass); } Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  68. 68. The login sentry can be injected. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  69. 69. That means, it can be replaced by a test double. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  70. 70. Dependency Injection Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  71. 71. DI is a Magento 2 thing, right? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  72. 72. DI can be everywhere! Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  73. 73. Injecting Test Doubles in Magento 1 Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  74. 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. 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. 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. 77. Injected collaborators make for simple tests! Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  78. 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. 79. We can give descriptive names. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  80. 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. 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. 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. 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. 84. Back to the example code... Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  85. 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. 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. 87. Why does that improve testability? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  88. 88. Creating a mock with magic methods is ugly! Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  89. 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. 90. Much simpler: $mockEvent = $this->createMock(Event::class); Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  91. 91. Summary Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  92. 92. What makes code simple to test? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  93. 93. → Separation of Business Logic and Entry Points Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  94. 94. → Small classes Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  95. 95. → Encapsulation of Business Logic in specific classes Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  96. 96. → Delegation to injectable dependencies Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  97. 97. → Real methods over magic methods Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  98. 98. Is there more? Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  99. 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. 100. Lets keep these for another time. Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  101. 101. But most importantly... Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  102. 102. ...have fun writing tests! Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  103. 103. (tell you me comment) (ask? you me question) (thank you) Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp
  104. 104. http://mage2katas.com/ Writing Testable Code in Magento 1 and 2 - #MM16PL 2016-09-19 - twitter://@VinaiKopp

×