Using mocks to test controllers

In this recipe we will learn how to extend what we have covered in the previous recipe by using mocks, an indispensable tool for building powerful test cases.

Getting ready

To go through this recipe, we need unit tests already in place. Go through the previous recipe.

How to do it...

  1. Edit your app/tests/cases/controllers/articles_controller.test.php file and place the following code at the beginning, right before the declaration of the class ArticlesControllerTestCase:
    App::import('Controller', 'Articles'),
    class TestArticlesController extends ArticlesController {
    public $name = 'Articles';
    public $testRedirect = false;
    public function __construct() {
    parent::__construct();
    Configure::write('controllers.'.$this->name, $this);
    }
    public function beforeFilter() {
    if (isset($this->Session)) {
    App::import('Component', 'Session'),
    Mock::generate('SessionComponent'),
    $this->Session = new MockSessionComponent();
    }
    parent::beforeFilter();
    }
    public function redirect($url, $status = null, $exit = true) {
    $this->testRedirect = compact('url', 'status', 'exit'),
    if ($exit) {
    $this->autoRender = false;
    }
    }
    }
    
  2. While still editing the articles_controller.test.php file, add the following code at the beginning of the ArticlesControllerTestCase class, right below the declaration of the fixtures property:
    public function testAction($url, $params = array()) {
    $url = preg_replace('/^/articles//', '/test_articles/', $url);
    $result = parent::testAction($url, $params);
    $this->Articles = Configure::read('controllers.Articles'),
    return $result;
    }
    
  3. Add the following code at the beginning of the testView() method:
    $result = $this->testAction('/articles/view/0'),
    $this->assertTrue(!empty($this->Articles->testRedirect));
    $this->assertEqual($this->Articles->testRedirect['url'], array('action' => 'index'));
    
  4. Finally, add the following method to the end of the ArticlesControllerTestCase class:
    public function testVote() {
    $result = $this->testAction('/articles/vote/2', array(
    'data' => array(
    'Vote' => array(
    'user_id' => 1,
    'vote' => 1
    )
    )
    ));
    $this->assertTrue(!empty($this->Articles->testRedirect));
    $this->assertEqual($this->Articles->testRedirect['url'], array('action' => 'index'));
    $this->Articles->Session->expectOnce('setFlash', array('Vote placed'));
    $article = $this->Articles->Article->get(2);
    $this->assertTrue(!empty($article) && !empty($article['Article']));
    $this->assertTrue(!empty($article[0]) && !empty($article[0]['vote']));
    $this->assertEqual(number_format($article[0]['vote'], 1), 2.7);
    }
    

If you now browse to http://localhost/test.php, click on the Test Cases option under the App section in the left menu, and then click on the controllers / ArticlesController test case, you should see our unit test succeeding, as shown in the next screenshot:

How to do it...

How it works...

We start by extending the controller we intend to test so we can override its redirect() method, so that when that method is executed as part of our unit test, the browser is not redirected and we can instead use the redirect information to make our assertions.

If redirect() is called, we store the destination in a property named testRedirect, and instead of aborting the execution (which would abort the test case) we avoid the view from being rendered. This works properly because every time we called redirect() from our ArticlesController class, we stopped the action execution by issuing a return statement.

As there is no direct way to get the instance of the controller that was executed from our test case (see the section There's more in this recipe for an alternative approach), we need to keep a reference of the controller instance. We use CakePHP's Configure class to store the reference, so that it can then be easily obtained.

We also want to avoid using real session data as a result of our unit test. This means that we need to find a way to let CakePHP think that when a controller interacts with its Session component, everything behaves as expected, while still not really interacting with the browser session. We also want to be able to assert when a particular method in that component is executed.

Mocks provide a way for us to mimic the way a real object behaves, without actually performing the object's underlying logic. With the following lines of code in the controller's beforeFilter callback:

if (isset($this->Session)) {
App::import('Component', 'Session'),
Mock::generate('SessionComponent'),
$this->Session = new MockSessionComponent();
}

We are replacing the instance of CakePHP's Session component with a mocked version. This mocked version will allow the controller to use all the component's available methods (such as setFlash()) without actually performing the underlying call. Mock::generate() will by default generate a fully mocked object (all its underlying functionality will be ignored.) If we wanted to mock only parts of an object, we would need to generate a partial mock. For example, if we only wanted to mock the setFlash() method of the Session component while still maintaining the rest of its original methods, we would do:

Mock::generatePartial('SessionComponent', false, array('setFlash'));

Once we have a mocked object and a way to access it from our unit tests, we can use any of the following mock assertions methods to test if a method of a mocked object is called as expected:

  • expectAtLeastOnce(): Its first argument is the name of the method we expect to have executed, while the second optional argument is an array of parameters we expect that method to have received. This is used when the expected method is to be called at least once, but can still be executed more times.
  • expectNever(): Its first mandatory argument is the name of a method that we intend to ensure has not been executed on the mocked object.
  • expectOnce(): It behaves exactly as expectAtLeastOnce(), but makes sure the method is executed only once.

We proceed by overriding CakeTestCase's testAction() method so that whenever an URL for the ArticlesController class is requested, we change that URL to use our extended TestArticlesController class. Once the proper action is executed, we obtain the instance of the controller class and keep it in a property of the unit test named Articles so we can then refer to it.

We are now ready to test. We start by modifying the testView() method so we can test a redirect() call, by building a test to force an invalid record ID, and asserting that the controller's testRedirect property is set to the index action.

We finalize the recipe by implementing the testVote() method, which gives us a chance to test posting data (using the second argument of the testAction() method as described in the previous recipe), and asserting that the mocked Session class receives a call to its setFlash() method, with the right arguments.

The last part of this unit test uses the main model of our controller to fetch the created article, and make sure that it matches our posted data.

There's more...

While the method shown in this recipe is quite powerful, it is definitely not the only way to test controllers. We can also perform direct calls on the controller actions we intend to test by instantiating the controller class and making a manual call to the action.

However, this is not a straightforward operation, since it would require a proper initialization of our controller by following the same steps than those defined by CakePHP's Dispatcher class. Mark Story has produced a thorough article describing this approach at http://mark-story.com/posts/view/testing-cakephp-controllers-the-hard-way.

Mark Story has also published a follow-up article on manual testing of controllers, where he introduces mocks. It is definitely a good read, and it is available at http://mark-story.com/posts/view/testing-cakephp-controllers-mock-objects-edition.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.72.15