'Model\Entity',
'Table' => 'Model\Table',
'Controller' => 'Controller',
'Component' => 'Controller\Component',
'Behavior' => 'Model\Behavior',
'Helper' => 'View\Helper',
'Shell' => 'Console\Command',
'Cell' => 'View\Cell',
];
/**
* class types that methods can be generated for
*
* @var array
*/
public $classSuffixes = [
'entity' => '',
'table' => 'Table',
'controller' => 'Controller',
'component' => 'Component',
'behavior' => 'Behavior',
'helper' => 'Helper',
'shell' => 'Shell',
'cell' => 'Cell',
];
/**
* Internal list of fixtures that have been added so far.
*
* @var array
*/
protected $_fixtures = [];
/**
* Execution method always used for tasks
*
* @return void
*/
public function main($type = null, $name = null) {
parent::main();
if (empty($type) && empty($name)) {
return $this->outputTypeChoices();
}
if (empty($name)) {
return $this->outputClassChoices($type);
}
if ($this->bake($type, $name)) {
$this->out('Done');
}
}
/**
* Output a list of class types you can bake a test for.
*
* @return void
*/
public function outputTypeChoices() {
$this->out(
__d('cake_console', 'You must provide a class type to bake a test for. The valid types are:'),
2
);
$i = 0;
foreach ($this->classTypes as $option => $package) {
$this->out(++$i . '. ' . $option);
}
$this->out('');
$this->out('Re-run your command as Console/cake bake ');
}
/**
* Output a list of possible classnames you might want to generate a test for.
*
* @param string $type The typename to get classes for.
* @return void
*/
public function outputClassChoices($type) {
$type = $this->mapType($type);
$plugin = null;
if (!empty($this->plugin)) {
$plugin = $this->plugin;
}
$this->out(
__d('cake_console', 'You must provide a class to bake a test for. Some possible options are:'),
2
);
$options = $this->_getClassOptions($type);
$i = 0;
foreach ($options as $option) {
$this->out(++$i . '. ' . $option);
}
$this->out('');
$this->out('Re-run your command as Console/cake bake ' . $type . ' ');
}
/**
* Get the possible classes for a given type.
*
* @param string $namespace The namespace fragment to look for classes in.
* @return array
*/
protected function _getClassOptions($namespace) {
$classes = [];
$base = APP;
if ($this->plugin) {
$base = Plugin::path($this->plugin);
}
$path = $base . str_replace('\\', DS, $namespace);
$folder = new Folder($path);
list($dirs, $files) = $folder->read();
foreach ($files as $file) {
$classes[] = str_replace('.php', '', $file);
}
return $classes;
}
/**
* Completes final steps for generating data to create test case.
*
* @param string $type Type of object to bake test case for ie. Model, Controller
* @param string $className the 'cake name' for the class ie. Posts for the PostsController
* @return string|bool
*/
public function bake($type, $className) {
$fullClassName = $this->getRealClassName($type, $className);
if (!empty($this->params['fixtures'])) {
$fixtures = array_map('trim', explode(',', $this->params['fixtures']));
$this->_fixtures = array_filter($fixtures);
} elseif ($this->typeCanDetectFixtures($type) && class_exists($fullClassName)) {
$this->out(__d('cake_console', 'Bake is detecting possible fixtures...'));
$testSubject = $this->buildTestSubject($type, $fullClassName);
$this->generateFixtureList($testSubject);
}
$methods = [];
if (class_exists($fullClassName)) {
$methods = $this->getTestableMethods($fullClassName);
}
$mock = $this->hasMockClass($type, $fullClassName);
list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName);
$uses = $this->generateUses($type, $fullClassName);
$subject = $className;
list($namespace, $className) = namespaceSplit($fullClassName);
list($baseNamespace, $subNamespace) = explode('\\', $namespace, 2);
$this->out("\n" . __d('cake_console', 'Baking test case for %s ...', $fullClassName), 1, Shell::QUIET);
$this->Template->set('fixtures', $this->_fixtures);
$this->Template->set('plugin', $this->plugin);
$this->Template->set(compact(
'subject', 'className', 'methods', 'type', 'fullClassName', 'mock',
'realType', 'preConstruct', 'postConstruct', 'construction',
'uses', 'baseNamespace', 'subNamespace', 'namespace'
));
$out = $this->Template->generate('classes', 'test');
$filename = $this->testCaseFileName($type, $fullClassName);
if ($this->createFile($filename, $out)) {
return $out;
}
return false;
}
/**
* Checks whether the chosen type can find its own fixtures.
* Currently only model, and controller are supported
*
* @param string $type The Type of object you are generating tests for eg. controller
* @return bool
*/
public function typeCanDetectFixtures($type) {
$type = strtolower($type);
return in_array($type, ['controller', 'table']);
}
/**
* Construct an instance of the class to be tested.
* So that fixtures can be detected
*
* @param string $type The type of object you are generating tests for eg. controller
* @param string $class The classname of the class the test is being generated for.
* @return object And instance of the class that is going to be tested.
*/
public function buildTestSubject($type, $class) {
TableRegistry::clear();
if (strtolower($type) === 'table') {
list($namespace, $name) = namespaceSplit($class);
$name = str_replace('Table', '', $name);
if ($this->plugin) {
$name = $this->plugin . '.' . $name;
}
$instance = TableRegistry::get($name);
} else {
$instance = new $class();
}
return $instance;
}
/**
* Gets the real class name from the cake short form. If the class name is already
* suffixed with the type, the type will not be duplicated.
*
* @param string $type The Type of object you are generating tests for eg. controller.
* @param string $class the Classname of the class the test is being generated for.
* @return string Real classname
*/
public function getRealClassName($type, $class) {
$namespace = Configure::read('App.namespace');
if ($this->plugin) {
$namespace = Plugin::getNamespace($this->plugin);
}
$suffix = $this->classSuffixes[strtolower($type)];
$subSpace = $this->mapType($type);
if ($suffix && strpos($class, $suffix) === false) {
$class .= $suffix;
}
return $namespace . '\\' . $subSpace . '\\' . $class;
}
/**
* Map the types that TestTask uses to concrete types that App::classname can use.
*
* @param string $type The type of thing having a test generated.
* @return string
* @throws \Cake\Error\Exception When invalid object types are requested.
*/
public function mapType($type) {
$type = ucfirst($type);
if (empty($this->classTypes[$type])) {
throw new Error\Exception('Invalid object type.');
}
return $this->classTypes[$type];
}
/**
* Get methods declared in the class given.
* No parent methods will be returned
*
* @param string $className Name of class to look at.
* @return array Array of method names.
*/
public function getTestableMethods($className) {
$classMethods = get_class_methods($className);
$parentMethods = get_class_methods(get_parent_class($className));
$thisMethods = array_diff($classMethods, $parentMethods);
$out = [];
foreach ($thisMethods as $method) {
if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) {
$out[] = $method;
}
}
return $out;
}
/**
* Generate the list of fixtures that will be required to run this test based on
* loaded models.
*
* @param object $subject The object you want to generate fixtures for.
* @return array Array of fixtures to be included in the test.
*/
public function generateFixtureList($subject) {
$this->_fixtures = [];
if ($subject instanceof Table) {
$this->_processModel($subject);
} elseif ($subject instanceof Controller) {
$this->_processController($subject);
}
return array_values($this->_fixtures);
}
/**
* Process a model recursively and pull out all the
* model names converting them to fixture names.
*
* @param Model $subject A Model class to scan for associations and pull fixtures off of.
* @return void
*/
protected function _processModel($subject) {
$this->_addFixture($subject->alias());
foreach ($subject->associations()->keys() as $alias) {
$assoc = $subject->association($alias);
$name = $assoc->target()->alias();
if (!isset($this->_fixtures[$name])) {
$this->_processModel($assoc->target());
}
if ($assoc->type() === Association::MANY_TO_MANY) {
$junction = $assoc->junction();
if (!isset($this->_fixtures[$junction->alias()])) {
$this->_processModel($junction);
}
}
}
}
/**
* Process all the models attached to a controller
* and generate a fixture list.
*
* @param \Cake\Controller\Controller $subject A controller to pull model names off of.
* @return void
*/
protected function _processController($subject) {
$subject->constructClasses();
$models = [$subject->modelClass];
foreach ($models as $model) {
list(, $model) = pluginSplit($model);
$this->_processModel($subject->{$model});
}
}
/**
* Add class name to the fixture list.
* Sets the app. or plugin.plugin_name. prefix.
*
* @param string $name Name of the Model class that a fixture might be required for.
* @return void
*/
protected function _addFixture($name) {
if ($this->plugin) {
$prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.';
} else {
$prefix = 'app.';
}
$fixture = $prefix . $this->_fixtureName($name);
$this->_fixtures[$name] = $fixture;
}
/**
* Is a mock class required for this type of test?
* Controllers require a mock class.
*
* @param string $type The type of object tests are being generated for eg. controller.
* @return bool
*/
public function hasMockClass($type) {
$type = strtolower($type);
return $type === 'controller';
}
/**
* Generate a constructor code snippet for the type and class name
*
* @param string $type The Type of object you are generating tests for eg. controller
* @param string $fullClassName The full classname of the class the test is being generated for.
* @return array Constructor snippets for the thing you are building.
*/
public function generateConstructor($type, $fullClassName) {
list($namespace, $className) = namespaceSplit($fullClassName);
$type = strtolower($type);
$pre = $construct = $post = '';
if ($type === 'table') {
$className = str_replace('Table', '', $className);
$pre = "\$config = TableRegistry::exists('{$className}') ? [] : ['className' => '{$fullClassName}'];\n";
$construct = "TableRegistry::get('{$className}', \$config);\n";
}
if ($type === 'behavior' || $type === 'entity') {
$construct = "new {$className}();\n";
}
if ($type === 'helper') {
$pre = "\$view = new View();\n";
$construct = "new {$className}(\$view);\n";
}
if ($type === 'component') {
$pre = "\$registry = new ComponentRegistry();\n";
$construct = "new {$className}(\$registry);\n";
}
if ($type === 'shell') {
$pre = "\$this->io = \$this->getMock('Cake\Console\ConsoleIo');\n";
$construct = "new {$className}(\$this->io);\n";
}
if ($type === 'cell') {
$pre = "\$this->request = \$this->getMock('Cake\Network\Request');\n";
$pre .= "\t\t\$this->response = \$this->getMock('Cake\Network\Response');\n";
$construct = "new {$className}(\$this->request, \$this->response);\n";
}
return [$pre, $construct, $post];
}
/**
* Generate the uses() calls for a type & class name
*
* @param string $type The Type of object you are generating tests for eg. controller
* @param string $realType The package name for the class.
* @param string $fullClassName The Classname of the class the test is being generated for.
* @return array An array containing used classes
*/
public function generateUses($type, $fullClassName) {
$uses = [];
$type = strtolower($type);
if ($type === 'component') {
$uses[] = 'Cake\Controller\ComponentRegistry';
}
if ($type === 'table') {
$uses[] = 'Cake\ORM\TableRegistry';
}
if ($type === 'helper') {
$uses[] = 'Cake\View\View';
}
$uses[] = $fullClassName;
return $uses;
}
/**
* Get the file path.
*
* @return string
*/
public function getPath() {
$dir = 'Test/TestCase/';
$path = ROOT . DS . $dir;
if (isset($this->plugin)) {
$path = $this->_pluginPath($this->plugin) . $dir;
}
return $path;
}
/**
* Make the filename for the test case. resolve the suffixes for controllers
* and get the plugin path if needed.
*
* @param string $type The Type of object you are generating tests for eg. controller
* @param string $className The fully qualified classname of the class the test is being generated for.
* @return string filename the test should be created on.
*/
public function testCaseFileName($type, $className) {
$path = $this->getPath();
$namespace = Configure::read('App.namespace');
if ($this->plugin) {
$namespace = Plugin::getNamespace($this->plugin);
}
$classTail = substr($className, strlen($namespace) + 1);
$path = $path . $classTail . 'Test.php';
return str_replace(['/', '\\'], DS, $path);
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser() {
$parser = parent::getOptionParser();
$parser->description(
__d('cake_console', 'Bake test case skeletons for classes.')
)->addArgument('type', [
'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'),
'choices' => [
'Controller', 'controller',
'Table', 'table',
'Entity', 'entity',
'Helper', 'helper',
'Component', 'component',
'Behavior', 'behavior'
]
])->addArgument('name', [
'help' => __d('cake_console', 'An existing class to bake tests for.')
])->addOption('fixtures', [
'help' => __d('cake_console', 'A comma separated list of fixture names you want to include.')
]);
return $parser;
}
}