1

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?

Nick
  • 8,049
  • 18
  • 63
  • 107

2 Answers2

2

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.

Kaia Leahy
  • 325
  • 3
  • 8
  • That makes sense, and I got things working with your help. Thank you! – Nick Jun 19 '13 at 22:31
  • Would only work for public methods and that's another problem. Any similar solutions for protected/private methods? – ZurabWeb Sep 04 '15 at 13:20
  • @Piero You should not be testing private or protected methods. It's possible to do so with PHPUnit, but it's not considered good practice -- it breaks encapsulation. Tests are there to make sure that your public interfaces do what they should. See [this question](http://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones) for a more detailed discussion. – Kaia Leahy Sep 05 '15 at 14:59
  • @sethrin Thanks, noted. – ZurabWeb Sep 08 '15 at 15:48
0

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.

floriank
  • 25,546
  • 9
  • 42
  • 66