* Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org) * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice * * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org) * @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests * @package Cake.TestSuite * @since CakePHP(tm) v 2.0 * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ PHP_CodeCoverage_Filter::getInstance()->addFileToBlacklist(__FILE__, 'DEFAULT'); App::uses('Dispatcher', 'Routing'); App::uses('CakeTestCase', 'TestSuite'); App::uses('Router', 'Routing'); App::uses('CakeRequest', 'Network'); App::uses('CakeResponse', 'Network'); App::uses('Helper', 'View'); /** * ControllerTestDispatcher class * * @package Cake.TestSuite */ class ControllerTestDispatcher extends Dispatcher { /** * The controller to use in the dispatch process * * @var Controller */ public $testController = null; /** * Use custom routes during tests * * @var boolean */ public $loadRoutes = true; /** * Returns the test controller * * @return Controller */ function _getController($request, $response) { if ($this->testController === null) { $this->testController = parent::_getController($request, $response); } $this->testController->helpers = array_merge(array('InterceptContent'), $this->testController->helpers); $this->testController->setRequest($request); $this->testController->response = $this->response; return $this->testController; } /** * Loads routes and resets if the test case dictates it should * * @return void */ protected function _loadRoutes() { parent::_loadRoutes(); if (!$this->loadRoutes) { Router::reload(); } } } /** * InterceptContentHelper class * * @package Cake.TestSuite */ class InterceptContentHelper extends Helper { /** * Intercepts and stores the contents of the view before the layout is rendered * * @param string $viewFile The view file */ public function afterRender($viewFile) { $this->_View->_viewNoLayout = $this->_View->output; $this->_View->Helpers->unload('InterceptContent'); } } /** * ControllerTestCase class * * @package Cake.TestSuite */ abstract class ControllerTestCase extends CakeTestCase { /** * The controller to test in testAction * * @var Controller */ public $controller = null; /** * Automatically mock controllers that aren't mocked * * @var boolean */ public $autoMock = false; /** * Use custom routes during tests * * @var boolean */ public $loadRoutes = true; /** * The resulting view vars of the last testAction call * * @var array */ public $vars = null; /** * The resulting rendered view of the last testAction call * * @var string */ public $view = null; /** * The resulting rendered layout+view of the last testAction call * * @var string */ public $contents = null; /** * The returned result of the dispatch (requestAction), if any * * @var string */ public $result = null; /** * The headers that would have been sent by the action * * @var string */ public $headers = null; /** * Used to enable calling ControllerTestCase::testAction() without the testing * framework thinking that it's a test case * * @param string $name The name of the function * @param array $arguments Array of arguments * @return Function */ public function __call($name, $arguments) { if ($name == 'testAction') { return call_user_func_array(array($this, '_testAction'), $arguments); } } /** * Tests a controller action. * * ### Options: * * - `data` POST or GET data to pass * - `method` POST or GET * * @param string $url The url to test * @param array $options See options */ protected function _testAction($url = '', $options = array()) { $this->vars = $this->result = $this->view = $this->contents = $this->headers = null; $options = array_merge(array( 'data' => array(), 'method' => 'POST', 'return' => 'result' ), $options); $_SERVER['REQUEST_METHOD'] = strtoupper($options['method']); if (strtoupper($options['method']) == 'GET') { $_GET = $options['data']; $_POST = array(); } else { $_POST = $options['data']; $_GET = array(); } $request = new CakeRequest($url); $Dispatch = new ControllerTestDispatcher(); foreach (Router::$routes as $route) { if ($route instanceof RedirectRoute) { $route->response = $this->getMock('CakeResponse', array('send')); } } $Dispatch->loadRoutes = $this->loadRoutes; $request = $Dispatch->parseParams($request); if (!isset($request->params['controller'])) { $this->headers = Router::currentRoute()->response->header(); return; } if ($this->controller !== null && Inflector::camelize($request->params['controller']) !== $this->controller->name) { $this->controller = null; } $plugin = empty($request->params['plugin']) ? '' : Inflector::camelize($request->params['plugin']) . '.'; if ($this->controller === null && $this->autoMock) { $this->generate(Inflector::camelize($plugin . $request->params['controller'])); } $params = array(); if ($options['return'] == 'result') { $params['return'] = 1; $params['bare'] = 1; $params['requested'] = 1; } $Dispatch->testController = $this->controller; $Dispatch->response = $this->getMock('CakeResponse', array('send')); $this->result = $Dispatch->dispatch($request, $Dispatch->response, $params); $this->controller = $Dispatch->testController; if ($options['return'] != 'result') { if (isset($this->controller->View)) { $this->vars = $this->controller->View->viewVars; $this->view = $this->controller->View->_viewNoLayout; } $this->contents = $this->controller->response->body(); } $this->headers = $Dispatch->response->header(); return $this->{$options['return']}; } /** * Generates a mocked controller and mocks any classes passed to `$mocks`. By * default, `_stop()` is stubbed as is sending the response headers, so to not * interfere with testing. * * ### Mocks: * * - `methods` Methods to mock on the controller. `_stop()` is mocked by default * - `models` Models to mock. Models are added to the ClassRegistry so they any * time they are instatiated the mock will be created. Pass as key value pairs * with the value being specific methods on the model to mock. If `true` or * no value is passed, the entire model will be mocked. * - `components` Components to mock. Components are only mocked on this controller * and not within each other (i.e., components on components) * * @param string $controller Controller name * @param array $mocks List of classes and methods to mock * @return Controller Mocked controller */ public function generate($controller, $mocks = array()) { list($plugin, $controller) = pluginSplit($controller); if ($plugin) { App::uses($plugin . 'AppController', $plugin . '.Controller'); $plugin .= '.'; } App::uses($controller . 'Controller', $plugin . 'Controller'); if (!class_exists($controller.'Controller')) { throw new MissingControllerException(array('controller' => $controller.'Controller')); } ClassRegistry::flush(); $mocks = array_merge_recursive(array( 'methods' => array('_stop'), 'models' => array(), 'components' => array() ), (array)$mocks); list($plugin, $name) = pluginSplit($controller); $_controller = $this->getMock($name.'Controller', $mocks['methods'], array(), '', false); $_controller->name = $name; $request = $this->getMock('CakeRequest'); $response = $this->getMock('CakeResponse', array('_sendHeader')); $_controller->__construct($request, $response); $config = ClassRegistry::config('Model'); foreach ($mocks['models'] as $model => $methods) { if (is_string($methods)) { $model = $methods; $methods = true; } if ($methods === true) { $methods = array(); } ClassRegistry::init($model); list($plugin, $name) = pluginSplit($model); $config = array_merge((array)$config, array('name' => $model)); $_model = $this->getMock($name, $methods, array($config)); ClassRegistry::removeObject($name); ClassRegistry::addObject($name, $_model); } foreach ($mocks['components'] as $component => $methods) { if (is_string($methods)) { $component = $methods; $methods = true; } if ($methods === true) { $methods = array(); } list($plugin, $name) = pluginSplit($component, true); App::uses($name . 'Component', $plugin . 'Controller/Component'); if (!class_exists($name . 'Component')) { throw new MissingComponentFileException(array( 'file' => $name . 'Component.php', 'class' => $name.'Component' )); } $_component = $this->getMock($name.'Component', $methods, array(), '', false); $_controller->Components->set($name, $_component); } $_controller->constructClasses(); $this->controller = $_controller; return $this->controller; } }