writing testable code (for magento 1 and 2) 2016 romaina

110
Writing Testable Code (for Magento 1 and 2) Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Upload: vinaikopp

Post on 08-Jan-2017

524 views

Category:

Software


0 download

TRANSCRIPT

Writing Testable Code(for Magento 1 and 2)

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Assumptions!

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

You know basic PHPUnit.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

In short, you want→ Testable code

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

When is code "testable"?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

When testingis simple & easy.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What makes atest simple?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

It is simple to write.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

It is easy to read.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What does"easy to read"

mean?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

It's intent is clear.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

The test is short.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Good names.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

It only doesone thing.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Clean Codeis for

Production Code& Test Code

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What does"simple to write"

mean?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Test codedepends on

production codeWriting Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

It are properties of the

production codethat make testing

easy or hard.Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What does easy to test code look like?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Magento 1 Example:Event Observer(Legacy Code)

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

<?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

Are we going to writeUnit Tests?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Hell NO!Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Unit tests only providevalue for new code.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

For previously untested code,Integration Tests

are much more valuable.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What would make it simpler to test?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

If the class wheresmallerit would be simpler to test.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

First attempt:Splitting the class based on purpose.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What does theclass do?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

Lets split it intoNetzarbeiter_CustomerActivation_Model...

..._Observer_ProhibitInactiveLogins

..._Observer_EmailNotifications

..._Observer_AdminhtmlCustomerGrid

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

<?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

<?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

<?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

Is this simplerto test?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Only minor difference in

testing effort.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Why?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

The same tests as before.Only split into three classes.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Second attempt:Lets go beyond

superficial changes.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Lets look at the design.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What collaborators are used?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Collaborators:Netzarbeiter_CustomerActivation_Helper_DataMage_Customer_Model_CustomerMage_Customer_Model_SessionMage_Customer_Model_GroupMage_Customer_Helper_AddressMage_Customer_Model_Resource_Customer_CollectionMage_Core_Controller_Request_HttpMage_Core_Controller_Response_HttpMage_Core_ExceptionMage_Core_Model_SessionMage_Core_Model_StoreMage_Sales_Model_Quote_AddressMage_Sales_Model_QuoteMage_Eav_Model_ConfigMage_Eav_Model_Entity_TypeMage_Adminhtml_Block_Widget_Grid_MassactionMage_Adminhtml_Block_Widget_Grid

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Almost all of them are core classes.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Only two classes are part of the module:Netzarbeiter_CustomerActivation_Model_ObserverNetzarbeiter_CustomerActivation_Helper_Data

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Based on the names, why do these classes exist?

Netzarbeiter_CustomerActivation_Model_ObserverNetzarbeiter_CustomerActivation_Helper_Data

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

The names don't tell us anything.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Extract parts by giving them

meaningful names

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

But where to start?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Separate business logicfrom entry points.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

Entry points:→ Observers→ Plugins

→ Controllers→ Cron Jobs→ Preferences

→ Console CommandsWriting Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Entry points linkBusiness logic

!Magento

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Remove allBusiness Logic

fromEntry Points.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What are the benefits?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

In our example,what is the

entry point?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Observer

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Old Observer Code:

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

New Code, without business logic:

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

And this class issimpler to test?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Yes, as there ismuch less logic.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Most of the logic is delegated to collaborators.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

$this->getCustomerLoginSentry()->abortLoginIfNotActive( $event->getData('customer'));

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

How does the delegation look in detail?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

private static $sentryClass = 'customeractivation/customerLoginSentry';

/** * @return CustomerLoginSentry */private function getCustomerLoginSentry(){ return $this->loginSentry ?? Mage::getModel(self::$sentryClass);}

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

The login sentry can be injected.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

That means,

it can be replaced by a test double.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Dependency Injection

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

DI is a Magento 2 thing, right?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

DI can be everywhere!

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Injecting Test Doublesin Magento 1

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

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

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_OWriting Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Injected collaboratorsmake for simple tests!

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Delegation allow us to createclasses with a specific purpose.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

We can give descriptive names.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Model with one responsibility:class Netzarbeiter_CustomerActivation_Model_CustomerLoginSentry

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

This business logic is now independent of the entry point.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

Back to the example code...

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

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

Why does thatimprove testability?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Creating a mock withmagic methods is ugly!

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

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

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

Much nicer:$mockEvent = $this->createMock(Event::class);

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

SummaryWriting Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

What makes codesimple to test?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Separation ofBusiness Logic

fromEntry Points

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Small classes(and methods)

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Encapsulation of Business Logic inspecific classes

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Delegation to injectable dependencies

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Favoringreal methods

overmagic methods

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Is there more?

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Yes!Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Adherence to the Law of Demeter.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Separation of methods that causesside effects from

methods returning values.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Avoidance of method call chaining.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

→ Methods having a single level of detail.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

Lets keep these for another time.

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

So most importantly...

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

...have fun writing tests!

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

(tell you me comment)(ask? you me question)

(thank you)

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp

http://mage2katas.com/

Writing Testable Code in Magento 1 and 2 - #MM16RO 2016-10-28 - twitter://@VinaiKopp