View helpers are a key point of the Mvc implementation in Zend Framework, along with the Zend_Controller component. The responsibility of view helpers is to keep programming logic out of the view scripts, which are rendered by the view object. We are talking about the presentational layer: every big of logic kept in a view helper can be reused in other scripts.
View helpers are instantiated on the fly by the view object (a Zend_View instance, or another Zend_View_Interface implementation) and kept there. This object will then include the scripts in a method, providing access to its scope to call fake methods with the name of view helpers (a __call() implementation). Since the scope of the object provides the $this handler, view scripts reference it to call view helpers:
<?php echo $this->doctype(); ?> <html> ...This script takes advantage of the Doctype view helper (a Zend_View_Helper_Doctype instance) to produce an html doctype declaration.
You can also write your own view helpers: according to the manual, they should provide an empty constructor to allow instantiation by the view object and a method which name corresponds to the class base name. For instance doctype() is the strategy method of Zend_View_Helper_Doctype.
The view helper class should implement Zend_View_Helper_Interface, which has the only injection point in this architecture, the setView() method.
The empty constructor is the problem in view helper management, since it gets in the way of simple test code when you write view helpers that make use of other view helpers as collaborators. For instance, I wrote yesterday a IconLoader helper which use the standard HeadStyle one to add css rules to the page.
The injection point which I was talking about, the setView() method, is called after instantiation. Once my IconLoader helper is instantiated, the view object injects itself in the helper using this method, providing a reference to other helpers.
In this design, the view acts as a Service Locator, and we have no idea which helpers could be called by another one: every helper class could depend on everything else.
Unit testing prescribes to test the helper class in isolation, substituting the collaborators with mocks or stubs. We have to put in test doubles as collaborators, otherwise we are testing more than one helper at the time and if the test fails we cannot tell if the problem is in the SUT (IconLoader) or in the referenced helpers (HeadStyle, ...). Note that collaborators can reference more collaborators, and soon IconLoader class can depend on the entire framework.
My very-simple-testing solution would be require helpers to specify their collaborators in the constructor or via setters, implementing Dependency Injection. We cannot change the standard Zend Framework architecture, though, but it's not a framework's fault. This solution would have required to build a small automatic dependency injection system, which is not in the scope of Zend Framework 1.x (but will be in 2.x as far as I know).
With the current architecture, we can make different choices to simplify testing (remember an helper's constructor must not have parameters, and we must test our components in isolation):
- Use the real view object and the real view helpers as collaborators. This is integration testing, and failures on the collaborators or view object or what else they refer to will make my main test fail too. Moreover, at every test you should bootstrap all the Zend Framework's Mvc system, so it's not a viable solution.
- Providing setters for collaborators. The view object cannot call these setters for us, so in the bootstrap we should require the view helper from the already set up view object and call the setters with the mandatory collaborators. In my example, I would have created a My_Helper_IconLoader::setHeadStyle() method. This is the simplest solution for testability, since we have only to call setters in the test for IconLoader view helper and passing in mocks; however, it requires to instantiate all view helpers which need collaborators at every page request, so it's a bit heavy but mandatory if the collaborators are not view helpers but other complex service classes. Starting to mock collaborators is right, though.
- Mocking the view object/Service Locator with phpunit. This can be done for one view helper, by setting an expectation on a (mocked too) view object __call() method. But when using multiple helpers as collaborator, phpunit cannot distinguish between calls with different parameters and decide which collaborator helper to return. It's an hack which would not work very well.
- Provide a Fake view object. This is my choice: write once a fake view class, which implements Zend_View_Interface but instead of creating view helpers only returns the ones set at construction time. The definition of a fake object is a class with a running implementation, but different from the production one, often a simplified implementation used for testing.
$this->_headStyleMock = $this->getMock('Zend_View_Helper_HeadStyle', array('appendStyle')); $view = new View(); $view->setHelper('headStyle', $this->_headStyleMock); $this->_helper = new IconLoader(); $this->_helper->setView($view);and after this injection, I can set expectations on $this->_headStyleMock and exercise IconLoader in the test methods.
You can find the View fake class at:
http://nakedphp.svn.sourceforge.net/viewvc/nakedphp/trunk/tests/NakedPhp/Stubs/View.php?revision=52&view=markup
along with some tests on it, which could help you grasp its usage:
http://nakedphp.svn.sourceforge.net/viewvc/nakedphp/trunk/tests/NakedPhp/Stubs/ViewTest.php?revision=52&view=markup
Obviously this fake class was Test-Driven Developed.
Feel free to ask any questions. I care about (unit) testability and using extensively frameworks can often make difficult the TDDer life.
Thanks for the great Post – very COOL!!!
ReplyDeleteGood post....
ReplyDeleteI found this also helpful:
http://stackoverflow.com/questions/6210695/is-it-possible-using-phpunit-mock-objects-to-expect-a-call-to-a-magic-call
Thanks.