Magento code testability: Problems and Solutions

Preview:

Citation preview

Magento code testability: Problems and Solutions

Unit testing

One of the main reasons for unit testing is improvement of code quality.

Unit tests are indicators that instantly show all the defects of object oriented code.

If code is hard to test – its quality is questionable.

Code flaws in magento detected by unit tests

• Fat constructors• Method complexity• Poor OOD (God objects)• Law of Demeter violations• Global State and Behavior usage• …

Fat constructors

Objects that have a lot of behavior in constructors are hard to test.

You’ve just created the object and it already created other objects, made some global calls, changed some global state, etc.

Fat constructors » Solution

Bad: mock all dependencies, create constructor tests and test all scenarios of constructor.

Good: Move all behavior from constructors. Leave only data initialization code.

Method Complexity

Unit tests test behavior scenarios. Unit testing paradigm requires every scenario covered by separate test.

Each flow control statement adds scenario to method, so complex methods with many control structures and protected calls require a lot of tests and mocks/stubs.

Method Complexity » Solution

Bad: For protected calls – use reflection or inheritance to test protected behavior in isolation. Write test per each scenario.

Good: Extract behavior from complex methods to separate objects that have small dependencies and are easily testable. Substitute conditions with polymorphism.

Poor OOD (God objects)

If an object has too many responsibilities there is a big chance that it will have internal calls between its public methods.

This creates problems for testing. Developer has to mock whole object to stub internal public calls, otherwise he will have test duplication.

Poor OOD (God objects) » Solution

Bad: Mock tested object and stub internal public calls.

Good: Extract small objects that will have their own responsibilities to avoid internal public calls.

Law of Demeter Violations

When tested object receives some context object that is used only to gain access to third service object the Law of Demeter is violated.

To test such code one would have to stub context object only to return service object. If the chains of calls are long, testing becomes problematic.

Law of Demeter Violations » Solution

Bad: Create mocks for context objects that will return themselves on every call except predefined stubbed calls.

Good: Refactor code to eliminate context objects. Depend only on objects that are required for delivering business goals of objects under test.

Global State And Behavior

Global mutable state is a reason for most bugs. It is unreliable for code that uses it.

Global state decreases code testability. Code that uses global state can not be tested in isolation. Developer must reproduce global environment of a unit to test it.

Global behavior is a killer of testability. It can not be mocked or stubbed for testing.

Global State And Behavior in Magento

• Global arrays• Global variables• Global factories• Mix (state + behavior)

Global State » Arrays

Mage::registry, Mage::register and Mage::unregister form an interface of global dynamic service locator simply wrapping mutable array with global behavior that restricts access to array but makes it harder to emulate in testing environment

Global State » Objects And Variables

• Mage::app()• Mage::getConfig()• Mage::get/setIsdeveloperMode()• Mage::getIsDownloader()

Global State » Factories

Global factories localize important part of application behavior – object creation. They eliminate direct dependencies in code. Which is good.

But instead of implicitly depending on created objects, code that uses global factories starts to implicitly depend on them.

Also global factories cannot be substituted in test environments.

Global State » Factories » Examples

• Mage::getModel()• Mage::getResourceModel()• Mage::getControllerInstance()

Global State » Mix (Behavior + State)

These are dangerous in code and are hard to substitute in tests:

• Mage::getSingleton()• Mage::helper()• Mage::getResourceHelper()

Global Behavior and State » Big deal

The big problem with global state and behavior in Magento is it’s used everywhere. All the dependencies of objects are pulled from global state instead of being pushed (injected) into these objects.

We cannot simply refactor our code to eliminate global dependencies. We would have to rewrite magento.

Global Behavior and State » Solution 1

Build “Magento Unit Testing Framework” on top of PHPUnit, write testing-environment-special Mage, make it “mockable” and test our code “in isolation”.

This is an absolutely viable solution that will let us unit test our code in isolation avoiding massive refactoring.

The problem

The problem with this solution is the same as with bad solutions described in previous sections: It fights the symptom (code non-testability), not the disease (global state).

And mutable global state and behavior is the reason of hard to debug type of bugs and encourages bad practices.

The problem » Bad practices

• Liar interfaces• Law of Demeter violations• Deep code dependencies• Liskov substitution principle violations• Separation of concerns violations• Data envy• ….

Global State » Solution

• Declare all object dependencies explicitly as entries in constructor argument array, and all the method-specific arguments as method’s parameters instead of pulling them from global state. Use object managers instead of global arrays when needed.

It will make our interfaces honest. And most code smells will become visible by only looking at method signatures. LSP violations will be noted by interpreter.

Global Behavior » Solution

• If an object must create other objects then it must declare dependency on object factory, that will be injected into the object.

It will instantly show objects that create too much.

Global Behavior » Solution

• If an object must create other objects then it must declare dependency on object factory, that will be injected into the object.

It will instantly show objects that create too much.

Global usage of Global » Solution

• All the constructors will check presence of dependencies in arguments, if an argument is not present – it is taken from Global.

It will let us avoid massive refactorings. Because developer will only have to refactor the object he tests – not all the code that uses it. This can be a stage of transforming magento code to prepare it for DiC.

Sources

• http://misko.hevery.com/• http://martinfowler.com/articles/injection.html• Test-Driven Development by Example. Kent

Beck

Anton KrilKyiv Dev Folks teamakril@ebay.com

Author