- Part 1 – Getting Started
- Part 2 – Testing Controllers
- Part 3 – Mocks and Stubs
- Part 4 – Wrapping up the controller
In the last part we tested for the form elements on the home page to meet the first of our requirements. In this part we’ll take on a few more requirements starting with the requirement that the form should be redisplayed when I submit invalid details.
For the details to be invalid it means we have validated the data and it failed validation. However our validation code is not done yet but we still need to ensure the controller and view do what we require when the details are invalid.
Zend_Form straddles both the view and model when it comes to MVC and seperation of concerns. We have used the view part to display the elements. To validate data we use the model part of Zend_Form and the isValid() method to check the validation status. The controller need not know what makes data valid and so to test the controller behaviour with valid and invalid data we have to simulate a form with passing validation and one with failing validation. This is where mocks and stubs come in. They are actually interchangeable (most of the time) but I’ll highlight the differences later on.
The first step to simulating Zend_Form validation results is to extract the form from the controller action into its own class. I’ll create a Subscribe.php file in the forms folder and the module autoloader should then autoload it.
<?php // application/forms/Subscribe.php class Default_Form_Subscribe extends Zend_Form { public function init() { $this->setAttrib('id', 'subscribe-form'); $this->addElement('text', 'fullname', array( 'label' => 'Full Name' )); $this->addElement('text', 'email', array( 'label' => 'Email Address' )); $this->addElement('submit', 'submit', array( 'label' => 'Subscribe' )); } }
The controller index action then becomes:
public function indexAction() { $form = new Default_Form_Subscribe(); $this->view->form = $form; }
We should still get passing tests when we run the test suite.
This highlights one very important advantage TDD gives us. We can refactor code with confidence without breaking functionality as we’ve got the tests in place to watch our backs.
However even with this change, we still cannot simulate the form class. To do that we have to inject our own version of the form class with our preferred output from the isValid() method. We are still creating an instance of the class in the action method and the only way we can replace the form instance is to create it elsewhere.
Here, the factory pattern comes to our rescue – rather than create the object where we need it, we can get an external form factory class to create it and fetch it from the index action. We’ll first create a test for the factory class to act as a guide.
<?php // tests/application/forms/SubscribeFormFactoryTest.php require_once realpath(dirname(__FILE__) . '/../../TestHelper.php'); class SubscribeFormFactoryTest extends PHPUnit_Framework_TestCase { public function testShouldCreateForm() { $form = Default_Form_SubscribeFactory::create(); $this->assertNotNull($form); $this->assertType('Default_Form_Subscribe', $form); } }
This class extends the PHPUnit_Framework_TestCase rather than the BaseControllerTestCase as we don’t need all the controller stuff. I have also defined a static create method which should return an instance of our form.
We get a ‘Fatal Error ‘ when we try running the test suite as the class and method don’t exist. I’ll add them now:
<?php class Default_Form_SubscribeFactory { public static function create() { return new Default_Form_Subscribe(); } }
We once again get passing tests when we run our test suite. We now need to be able to inject a form into the factory class and when we call the create method we get our form instance rather than the default one. I’ll add a test for that:
public function testShouldSetCustomFormInstance() { $mockForm = $this->getMock('Default_Form_Subscribe'); Default_Form_SubscribeFactory::setForm($mockForm); $form = Default_Form_SubscribeFactory::create(); $this->assertEquals($mockForm, $form); }
PHPUnit provides a getMock() method to set up mock objects. Mock Objects are simulated objects that mimic the behavior of real objects in controlled ways [wikipedia] and are useful in unit tests when it’s impossible or not practical to use the real object. For a primer in using mocks with unit tests check out this post on CodeUtopia.
For our test, we create a mock form object, inject it into our form factory and assert we get our custom instance back when we call the create() method. We don’t have an implementation so our test suite should fail when we run it.
I’ll add the following implementation to get the tests to pass.
<?php class Default_Form_SubscribeFactory { private static $_form = null; public static function create() { if (is_null(self::$_form)) { self::$_form = new Default_Form_Subscribe(); } return self::$_form; } public static function setForm($form) { self::$_form = $form; } }
Once again we get passing tests and our subscribe form factory is complete.
To use our form factory in the Index controller I’ll change the index action to:
<?php class IndexController extends Zend_Controller_Action { public function indexAction() { $form = Default_Form_SubscribeFactory::create(); $this->view->form = $form; } }
An alternative is to use a Dependency Injection framework to load any controller dependencies. Two popular ones in the ZF community are Yadif and Symfony Dependency Injection
We still get passing tests when we run our test suite and we can replace the form instance if we need to.
We can now add our test to verify the behaviour when the details are invalid.
public function testShouldRedisplayFormOnInvalidEntry() { $subscribeForm = $this->getMock('Default_Form_Subscribe', array('isValid')); $subscribeForm->expects($this->any())->method('isValid') ->will($this->returnValue(false)); Default_Form_SubscribeFactory::setForm($subscribeForm); $this->request->setMethod('POST'); $this->dispatch('/index/subscribe'); $this->assertNotRedirect(); $this->assertQuery('form[id="subscribe-form"]'); }
Here I am creating a mock form object and stubbing the isValid method to return false (simulating invalid details). Using our form factory, we then inject our version of the form into the controller. The second parameter of the getMock method is optional and specifies the methods we need to mock. If left blank, all methods will be mocked but in our case we don’t want that as we later assert the form elements are displayed. The request method is changed to a post and I assert that there is no redirect and that the form is redisplayed.
This test fails! We are dispatching to an action we haven’t created and so that’s expected. We’ll add the subscribe action now.
public function subscribeAction() { $form = Default_Form_SubscribeFactory::create(); if (!$form->isValid($this->_request->getPost())) { $this->view->form = $form; $this->render('index'); } }
Our tests now pass. Our app is redisplaying the form when the details are invalid. However what should happen when we send valid details. From our specs, two things should happen:
- I should be redirected to a thank you page.
- The submitted details should be saved to the database.
We’ll write a test for the first action.
public function testShouldRedirectToThankYouPageOnValidEntry() { $subscribeForm = $this->getMock('Default_Form_Subscribe', array('isValid')); $subscribeForm->expects($this->any())->method('isValid') ->will($this->returnValue(true)); Default_Form_SubscribeFactory::setForm($subscribeForm); $this->request->setMethod('POST'); $this->dispatch('/index/subscribe'); $this->assertRedirectTo('/index/thanks'); }
Our tests fail again and we change the subscribe action to the version below to get them passing again.
public function subscribeAction() { $form = Default_Form_SubscribeFactory::create(); if (!$form->isValid($this->_request->getPost())) { $this->view->form = $form; $this->render('index'); } else { $this->_redirect('index/thanks'); } }
Let’s now write a test to verify the details get saved to the database. This is another tricky one. It’s not the controller’s job to save the details to the database and so it has to call a method in a model class to do that. However, we don’t have the model class written yet so we’ll have to come up with an appropriate method signature. This illustrates how TDD helps us flesh out our application’s API.
Once again, we don’t really care how the model gets the job done and so we’ll mock out the model and add a model factory like we did with the form. I’ll fast forward this section and just show the implementation (it’s basically the same as the form – although we will revisit it when we need to add dependencies).
<?php // tests/application/models/SubscriberModelFactoryTest.php require_once realpath(dirname(__FILE__) . '/../../TestHelper.php'); class SubscriberModelFactoryTest extends PHPUnit_Framework_TestCase { public function testShouldCreateModel() { $model = Default_Model_SubscriberFactory::create(); $this->assertNotNull($model); $this->assertType('Default_Model_Subscriber', $model); } public function testShouldSetCustomModelInstance() { $mockModel = $this->getMock('Default_Model_Subscriber'); Default_Model_SubscriberFactory::setModel($mockModel); $model = Default_Model_SubscriberFactory::create(); $this->assertEquals($mockModel, $model); } }
<?php // application/models/SubscriberFactory.php class Default_Model_SubscriberFactory { private static $_model = null; public static function create() { if (is_null(self::$_model)) { self::$_model = new Default_Model_Subscriber(); } return self::$_model; } public static function setModel($model) { self::$_model = $model; } }
Here again, in the real world, we’d use an abstract factory class or even better, a dependency injection framework but this will have to do for now.
Running our test suite at this point gives us an error telling us it can’t find the ‘Default_Model_Subscriber’ class. We’ll have to create it to move on.
<?php // application/models/Subscriber.php class Default_Model_Subscriber { }
We should now get passing tests.
We’ll now add a test for to verify the details are saved. We’ll assume the ‘Subscriber’ model class has a method call ‘save’ which saves the data to the database.
public function testShouldSaveDetailsOnValidEntry() { $subscribeForm = $this->getMock('Default_Form_Subscribe', array('isValid')); $subscribeForm->expects($this->any())->method('isValid') ->will($this->returnValue(true)); Default_Form_SubscribeFactory::setForm($subscribeForm); $subscriberModel = $this->getMock('Default_Model_Subscriber'); $subscriberModel->expects($this->once())->method('save'); Default_Model_SubscriberFactory::setModel($subscriberModel); $this->request->setMethod('POST'); $this->dispatch('/index/subscribe'); }
Running the test suite gives us the following error:
Expectation failed for method name is equal to <string:save> when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times.
And there lies the difference between mocks and stubs. Our previous usage of the PHPunit mock object have actually been stubs. With a stub, we are verifying state (we return a known response from a method call and verify the state of our app) while with a mock we are verifying behaviour (we expect the save method to be called). With the mock object we don’t even have to assert anything. The expectations will fail if they are not fulfilled.
To get our tests to pass we’ll add the call to the save method.
public function subscribeAction() { $form = Default_Form_SubscribeFactory::create(); if (!$form->isValid($this->_request->getPost())) { $this->view->form = $form; $this->render('index'); } else { $subscriberModel = Default_Model_SubscriberFactory::create(); $subscriberModel->save($form->getValues()); $this->_redirect('index/thanks'); } }
We get an error when we run the test suite – one of our other test cases is choking on the call to the save method that doesn’t exist. To fix that we’ll add an empty implementation.
<?php // application/models/Subscriber.php class Default_Model_Subscriber { public function save($data) { } }
Yay! Passing tests.
What Next?
We’re almost done with the controller. We need to add the thanks page and add a typical edge case verification – I’m assuming the subscribe action will always receive a POST. What happens if I GET the action?
There’s also a lot of repetition in our IndexControllerTest class while setting up our mocks. We’ll need to DRY this up.
We’ll take care of all this in the next section.
Resources
- Martin Fowler’s Mocks Aren’t Stubs
- PHPUnit Test Doubles
- Another take on using mocks with ZF controllers
- The Difference between Mocks and Stubs
- Mocks vs. Stubs and my ‘Ah ha’ moment
Related posts:
Pingback: blog.nielslange.de » Test Driven Development (TTD) with the Zend Framework