Nick
Nick

Reputation: 8319

Change CakePHP settings for tests

The Code

AppController.php

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

class AppController extends Controller {

    public $components = array(
        'Auth' => array(
            'authorize' => array('Controller')
        ),
        'Session'
    );
}

PostsController.php

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

class PostsController extends AppController {

    public function isAuthorized() {
        return $this->Auth->user('role') == 'admin';
    }

    public function add() {
        $this->set('some_var', true);
    }
}

PostsControllerTest.php

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

class PostsControllerTest extends ControllerTestCase {

    public function setUp() {
        parent::setUp();
        CakeSession::write('Auth.User', array(
            'id' => 2,
            'username' => 'joe_bloggs',
            'role' => 'user',
            'created' => '2013-05-17 10:00:00',
            'modified' => '2013-05-17 10:00:00'
        ));
    }

    public function testAddWhileLoggedInAsNonAdminFails() {
        $this->testAction('/posts/add/', array('method' => 'get'));
        $this->assertTrue($this->vars['some_var']);
    }

    public function tearDown() {
        parent::tearDown();
        CakeSession::destroy();
    }
}

The Problem

Right now, the "testAddWhileLoggedInAsNonAdminFails" test passes. It should fail. The issue is that redirects do not exit/halt the simulated request.

Partial Solution

I can fix the problem by modifying "AppController.php" and "PostsControllerTest.php" like so:

Modified AppController.php

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

class AppController extends Controller {

    public $components = array(
        'Auth' => array(
            'authorize' => array('Controller'),
            // ***** THE FOLLOWING LINE IS NEW *****
            'unauthorizedRedirect' => false
        ),
        'Session'
    );
}

Modified PostsControllerTest.php

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

class PostsControllerTest extends ControllerTestCase {

    public function setUp() {
        parent::setUp();
        CakeSession::write('Auth.User', array(
            'id' => 2,
            'username' => 'joe_bloggs',
            'role' => 'user',
            'created' => '2013-05-17 10:00:00',
            'modified' => '2013-05-17 10:00:00'
        ));
    }

// ***** THE FOLLOWING 3 LINES ARE NEW *****
/**
 * @expectedException ForbiddenException
 */
    public function testAddWhileLoggedInAsNonAdminFails() {
        $this->testAction('/posts/add/', array('method' => 'get'));
    }

    public function tearDown() {
        parent::tearDown();
        CakeSession::destroy();
    }
}

The problem with this solution is it modifies the behavior of the real website too. I'm looking for a way to set the Auth component's unauthorizedRedirect property to false only when tests are being run. How can I do this?

Upvotes: 1

Views: 400

Answers (2)

floriank
floriank

Reputation: 25698

Did you check the book? http://book.cakephp.org/2.0/en/development/testing.html#testing-controllers

When testing actions that contain redirect() and other code following the redirect it is generally a good idea to return when redirecting. The reason for this, is that redirect() is mocked in testing, and does not exit like normal. And instead of your code exiting, it will continue to run code following the redirect.

It exactly describes your problem.

I haven't tested this but try it, the manual says the controller is already mocked when using ControllerTestCase so you should be able to expect it:

$this->controller->expects($this->at(0))
    ->method('redirect')
    ->with('/your-expected-input');

Taking a look at the ControllerTestCase class might reveal how the controller is exactly mocked and set up. Alternatively you could just fall back to the regular CakeTestCase and set the controller mocks up by yourself.

Another alternative would be to extend your controller you want to test and override the redirect() method, not calling the parent but setting the first arg to a property like Controller::$redirectUrl. After your action call you can then assertEqual the properties value. But this still requires you to return after the redirect call in your controller. Also this won't work either when using ControllerTestCase because it would mock your overriden method.

Upvotes: 0

Kaia Leahy
Kaia Leahy

Reputation: 325

Changing the behavior of your code to make tests work right is not really a good idea.

The correct answer to this question is that it's not a very good question, and what you really should do is test each function separately.

For the isAuthorized function, you should do:

<?php
class PostsControllerTest extends ControllerTestCase {

    public function testIsAuthorized() {
        $Posts = $this->generate('Posts');
        $user = array('role' => 'admin');
        $this->assertTrue($Posts->isAuthorized($user));
        $anotherUser = array('role' => 'saboteur');
        $this->assertFalse($Posts->isAuthorized($user));
    }
    public function testAdd() {
        $this->testAction('/posts/add/', array('method' => 'get'));
        $this->assertTrue($this->vars['some_var']);
    }
}

The core concept behind unit testing is breaking down your app into the smallest pieces possible, and testing each in isolation. Once you have your unit tests sorted out, you can work on integration tests that cover more than one function, but many projects never reach that stage, and that's okay. The redirect issue can be interesting to work with, but you can mock out controller::redirect as described in this blog post. It's a bit old but still useful.

Upvotes: 2

Related Questions