Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CakePHP Controller Testing: Mocking the Auth Component

The Situation

Controller Code

<?php
App::uses('AppController', 'Controller');

class PostsController extends AppController {

    public function isAuthorized() {
        return true;
    }

    public function edit($id = null) {
        $this->autoRender = false;

        if (!$this->Post->exists($id)) {
            throw new NotFoundException(__('Invalid post'));
        }

        if ($this->Post->find('first', array(
            'conditions' => array(
                'Post.id' => $id,
                'Post.user_id' => $this->Auth->user('id')
            )
        ))) {
            echo 'Username: ' . $this->Auth->user('username') . '<br>';
            echo 'Created: ' . $this->Auth->user('created') . '<br>';
            echo 'Modified: ' . $this->Auth->user('modified') . '<br>';
            echo 'All:';
            pr($this->Auth->user());
            echo 'Modified: ' . $this->Auth->user('modified') . '<br>';
        } else {
            echo 'Unauthorized.';
        }
    }
}

Output from Browser

Username: admin
Created: 2013-05-08 00:00:00
Modified: 2013-05-08 00:00:00
All:

Array
(
    [id] => 1
    [username] => admin
    [created] => 2013-05-08 00:00:00
    [modified] => 2013-05-08 00:00:00
)

Modified: 2013-05-08 00:00:00

Test Code

<?php
App::uses('PostsController', 'Controller');

class PostsControllerTest extends ControllerTestCase {

    public $fixtures = array(
        'app.post',
        'app.user'
    );

    public function testEdit() {
        $this->Controller = $this->generate('Posts', array(
            'components' => array(
                'Auth' => array('user')
            )
        ));

        $this->Controller->Auth->staticExpects($this->at(0))->method('user')->with('id')->will($this->returnValue(1));
        $this->Controller->Auth->staticExpects($this->at(1))->method('user')->with('username')->will($this->returnValue('admin'));
        $this->Controller->Auth->staticExpects($this->at(2))->method('user')->with('created')->will($this->returnValue('2013-05-08 00:00:00'));
        $this->Controller->Auth->staticExpects($this->at(3))->method('user')->with('modified')->will($this->returnValue('2013-05-08 00:00:00'));
        $this->Controller->Auth->staticExpects($this->at(4))->method('user')->will($this->returnValue(array(
            'id' => 1,
            'username' => 'admin',
            'created' => '2013-05-08 00:00:00',
            'modified' => '2013-05-08 00:00:00'
        )));

        $this->testAction('/posts/edit/1', array('method' => 'get'));
    }
}

Output from Test

Username: admin
Created: 2013-05-08 00:00:00
Modified: 2013-05-08 00:00:00
All:

Array
(
    [id] => 1
    [username] => admin
    [created] => 2013-05-08 00:00:00
    [modified] => 2013-05-08 00:00:00
)

Modified: 

The Problem

There are actually three problems here:

  1. The test code is very repetitive.
  2. The second "Modified" line in the output from the test is blank. It should be "2013-05-08 00:00:00" like in the output from the browser.
  3. If I were to modify the controller code, adding a line that said echo 'Email: ' . $this->Auth->user('email') . '<br>'; (just for example) between the echoing of "Username" and "Created", the test would fail with this error: Expectation failed for method name is equal to <string:user> when invoked at sequence index 2. This makes sense since the $this->at(1) is no longer true.

My Question

How can I mock the Auth component in a way that (1) is not repetitive, (2) causes the test to output the same thing as the browser, and (3) allows me to add $this->Auth->user('foo') code anywhere without breaking the tests?

like image 238
Nick Avatar asked May 08 '13 18:05

Nick


1 Answers

Before I answer this I have to admit that I've no experience of using the CakePHP framework. However, I have a fair amount of experience working with PHPUnit in conjunction with the Symfony framework and have encountered similar issues. To address your points:

  1. See my answer to point 3

  2. The reason for this is that you need an additional ...->staticExpects($this->at(5))... statement to cover the 6th call to Auth->user(). These statements do not define the values to return for any call to Auth->user() with the specified value. They define that e.g. the 2nd call to the Auth object must be to method user() with parameter 'username' in which case 'admin' will be returned. However, this should no longer be an issue if you follow the approach in the next point.

  3. I am making the assumption that what you are trying to achieve here is to test your controller independently of the Auth component (because frankly it doesn't make sense to test that a controller makes a series of getter calls on a user object) . In this case a mock object is set up as a stub to always return a particular set of results, rather than to expect a specific series of calls with particular parameters (See PHP Manual entry on stubs). This could be done just be replacing '$this->at(x)' with '$this->any()' in your code. However, whilst this would negate the need to add the extra line I mentioned in point 2, you'd still have the repetition. Following the TDD approach of writing tests before code, I'd suggest the following:

    public function testEdit() {
        $this->Controller = $this->generate('Posts', array(
            'components' => array(
                'Auth' => array('user')
            )
        ));
            $this->Controller->Auth
                ->staticExpects($this->any())
                ->method('user')
                ->will($this->returnValue(array(
                    'id' => 1,
                    'username' => 'admin',
                    'created' => '2013-05-08 00:00:00',
                    'modified' => '2013-05-08 00:00:00',
                    'email' => '[email protected]',
                )));
    
        $this->testAction('/posts/edit/1', array('method' => 'get'));
    }
    

This would allow your controller to be updated to make as many calls as you like to get user attributes in any order provided they are already returned by the mock object. Your mock object could be written to return all user attributes (or perhaps all likely to be relevant to this controller) regardless of whether and how often the controller retrieves them. (Note in your specific example if your mock contains 'email' the pr() statement in the controller will output different results from the test than the browser but I am presuming you don't expect to be able to add new attributes to a record without having to update your tests).

Writing the test this way means your controller edit function would need to be something like this - a more testable version:

$this->autoRender = false;

if (!$this->Post->exists($id)) {
    throw new NotFoundException(__('Invalid post'));
}

$user = $this->Auth->user();

if ($this->Post->find('first', array(
    'conditions' => array(
        'Post.id' => $id,
        'Post.user_id' => Hash::get($user, 'id')
    )
))) {
    echo 'Username: ' . Hash::get($user, 'username') . '<br>';
    echo 'Created: ' . Hash::get($user, 'created') . '<br>';
    echo 'Modified: ' . Hash::get($user, 'modified') . '<br>';
    echo 'All:';
    pr($user);
    echo 'Modified: ' . Hash::get($user, 'modified') . '<br>';
} else {
    echo 'Unauthorized.';
}

As far as I can gather, Hash::get($record, $key) is the correct CakePHP way to retrieve attributes from a record although with the simple attributes you have here I presume user[$key] would work just as well.

like image 157
redbirdo Avatar answered Nov 12 '22 11:11

redbirdo