TestTask.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 1.3.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Console\Command\Task;
  16. use Cake\Console\Shell;
  17. use Cake\Controller\Controller;
  18. use Cake\Core\App;
  19. use Cake\Core\Configure;
  20. use Cake\Core\Plugin;
  21. use Cake\Error;
  22. use Cake\ORM\Association;
  23. use Cake\ORM\Table;
  24. use Cake\ORM\TableRegistry;
  25. use Cake\Utility\Folder;
  26. use Cake\Utility\Inflector;
  27. /**
  28. * Task class for creating and updating test files.
  29. *
  30. */
  31. class TestTask extends BakeTask {
  32. /**
  33. * Tasks used.
  34. *
  35. * @var array
  36. */
  37. public $tasks = ['Template'];
  38. /**
  39. * class types that methods can be generated for
  40. *
  41. * @var array
  42. */
  43. public $classTypes = [
  44. 'Entity' => 'Model\Entity',
  45. 'Table' => 'Model\Table',
  46. 'Controller' => 'Controller',
  47. 'Component' => 'Controller\Component',
  48. 'Behavior' => 'Model\Behavior',
  49. 'Helper' => 'View\Helper',
  50. 'Shell' => 'Console\Command',
  51. 'Cell' => 'View\Cell',
  52. ];
  53. /**
  54. * class types that methods can be generated for
  55. *
  56. * @var array
  57. */
  58. public $classSuffixes = [
  59. 'entity' => '',
  60. 'table' => 'Table',
  61. 'controller' => 'Controller',
  62. 'component' => 'Component',
  63. 'behavior' => 'Behavior',
  64. 'helper' => 'Helper',
  65. 'shell' => 'Shell',
  66. 'cell' => 'Cell',
  67. ];
  68. /**
  69. * Internal list of fixtures that have been added so far.
  70. *
  71. * @var array
  72. */
  73. protected $_fixtures = [];
  74. /**
  75. * Execution method always used for tasks
  76. *
  77. * @param string $type Class type.
  78. * @param string $name Name.
  79. * @return void
  80. */
  81. public function main($type = null, $name = null) {
  82. parent::main();
  83. if (empty($type) && empty($name)) {
  84. return $this->outputTypeChoices();
  85. }
  86. if (empty($name)) {
  87. return $this->outputClassChoices($type);
  88. }
  89. if ($this->bake($type, $name)) {
  90. $this->out('<success>Done</success>');
  91. }
  92. }
  93. /**
  94. * Output a list of class types you can bake a test for.
  95. *
  96. * @return void
  97. */
  98. public function outputTypeChoices() {
  99. $this->out(
  100. __d('cake_console', 'You must provide a class type to bake a test for. The valid types are:'),
  101. 2
  102. );
  103. $i = 0;
  104. foreach ($this->classTypes as $option => $package) {
  105. $this->out(++$i . '. ' . $option);
  106. }
  107. $this->out('');
  108. $this->out('Re-run your command as Console/cake bake <type> <classname>');
  109. }
  110. /**
  111. * Output a list of possible classnames you might want to generate a test for.
  112. *
  113. * @param string $type The typename to get classes for.
  114. * @return void
  115. */
  116. public function outputClassChoices($type) {
  117. $type = $this->mapType($type);
  118. $plugin = null;
  119. if (!empty($this->plugin)) {
  120. $plugin = $this->plugin;
  121. }
  122. $this->out(
  123. __d('cake_console', 'You must provide a class to bake a test for. Some possible options are:'),
  124. 2
  125. );
  126. $options = $this->_getClassOptions($type);
  127. $i = 0;
  128. foreach ($options as $option) {
  129. $this->out(++$i . '. ' . $option);
  130. }
  131. $this->out('');
  132. $this->out('Re-run your command as Console/cake bake ' . $type . ' <classname>');
  133. }
  134. /**
  135. * Get the possible classes for a given type.
  136. *
  137. * @param string $namespace The namespace fragment to look for classes in.
  138. * @return array
  139. */
  140. protected function _getClassOptions($namespace) {
  141. $classes = [];
  142. $base = APP;
  143. if ($this->plugin) {
  144. $base = Plugin::classPath($this->plugin);
  145. }
  146. $path = $base . str_replace('\\', DS, $namespace);
  147. $folder = new Folder($path);
  148. list($dirs, $files) = $folder->read();
  149. foreach ($files as $file) {
  150. $classes[] = str_replace('.php', '', $file);
  151. }
  152. return $classes;
  153. }
  154. /**
  155. * Completes final steps for generating data to create test case.
  156. *
  157. * @param string $type Type of object to bake test case for ie. Model, Controller
  158. * @param string $className the 'cake name' for the class ie. Posts for the PostsController
  159. * @return string|bool
  160. */
  161. public function bake($type, $className) {
  162. $fullClassName = $this->getRealClassName($type, $className);
  163. if (!empty($this->params['fixtures'])) {
  164. $fixtures = array_map('trim', explode(',', $this->params['fixtures']));
  165. $this->_fixtures = array_filter($fixtures);
  166. } elseif ($this->typeCanDetectFixtures($type) && class_exists($fullClassName)) {
  167. $this->out(__d('cake_console', 'Bake is detecting possible fixtures...'));
  168. $testSubject = $this->buildTestSubject($type, $fullClassName);
  169. $this->generateFixtureList($testSubject);
  170. }
  171. $methods = [];
  172. if (class_exists($fullClassName)) {
  173. $methods = $this->getTestableMethods($fullClassName);
  174. }
  175. $mock = $this->hasMockClass($type, $fullClassName);
  176. list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName);
  177. $uses = $this->generateUses($type, $fullClassName);
  178. $subject = $className;
  179. list($namespace, $className) = namespaceSplit($fullClassName);
  180. list($baseNamespace, $subNamespace) = explode('\\', $namespace, 2);
  181. $this->out("\n" . __d('cake_console', 'Baking test case for %s ...', $fullClassName), 1, Shell::QUIET);
  182. $this->Template->set('fixtures', $this->_fixtures);
  183. $this->Template->set('plugin', $this->plugin);
  184. $this->Template->set(compact(
  185. 'subject', 'className', 'methods', 'type', 'fullClassName', 'mock',
  186. 'realType', 'preConstruct', 'postConstruct', 'construction',
  187. 'uses', 'baseNamespace', 'subNamespace', 'namespace'
  188. ));
  189. $out = $this->Template->generate('classes', 'test');
  190. $filename = $this->testCaseFileName($type, $fullClassName);
  191. if ($this->createFile($filename, $out)) {
  192. return $out;
  193. }
  194. return false;
  195. }
  196. /**
  197. * Checks whether the chosen type can find its own fixtures.
  198. * Currently only model, and controller are supported
  199. *
  200. * @param string $type The Type of object you are generating tests for eg. controller
  201. * @return bool
  202. */
  203. public function typeCanDetectFixtures($type) {
  204. $type = strtolower($type);
  205. return in_array($type, ['controller', 'table']);
  206. }
  207. /**
  208. * Construct an instance of the class to be tested.
  209. * So that fixtures can be detected
  210. *
  211. * @param string $type The type of object you are generating tests for eg. controller
  212. * @param string $class The classname of the class the test is being generated for.
  213. * @return object And instance of the class that is going to be tested.
  214. */
  215. public function buildTestSubject($type, $class) {
  216. TableRegistry::clear();
  217. if (strtolower($type) === 'table') {
  218. list($namespace, $name) = namespaceSplit($class);
  219. $name = str_replace('Table', '', $name);
  220. if ($this->plugin) {
  221. $name = $this->plugin . '.' . $name;
  222. }
  223. $instance = TableRegistry::get($name);
  224. } else {
  225. $instance = new $class();
  226. }
  227. return $instance;
  228. }
  229. /**
  230. * Gets the real class name from the cake short form. If the class name is already
  231. * suffixed with the type, the type will not be duplicated.
  232. *
  233. * @param string $type The Type of object you are generating tests for eg. controller.
  234. * @param string $class the Classname of the class the test is being generated for.
  235. * @return string Real class name
  236. */
  237. public function getRealClassName($type, $class) {
  238. $namespace = Configure::read('App.namespace');
  239. if ($this->plugin) {
  240. $namespace = Plugin::getNamespace($this->plugin);
  241. }
  242. $suffix = $this->classSuffixes[strtolower($type)];
  243. $subSpace = $this->mapType($type);
  244. if ($suffix && strpos($class, $suffix) === false) {
  245. $class .= $suffix;
  246. }
  247. return $namespace . '\\' . $subSpace . '\\' . $class;
  248. }
  249. /**
  250. * Map the types that TestTask uses to concrete types that App::className can use.
  251. *
  252. * @param string $type The type of thing having a test generated.
  253. * @return string
  254. * @throws \Cake\Error\Exception When invalid object types are requested.
  255. */
  256. public function mapType($type) {
  257. $type = ucfirst($type);
  258. if (empty($this->classTypes[$type])) {
  259. throw new Error\Exception('Invalid object type.');
  260. }
  261. return $this->classTypes[$type];
  262. }
  263. /**
  264. * Get methods declared in the class given.
  265. * No parent methods will be returned
  266. *
  267. * @param string $className Name of class to look at.
  268. * @return array Array of method names.
  269. */
  270. public function getTestableMethods($className) {
  271. $classMethods = get_class_methods($className);
  272. $parentMethods = get_class_methods(get_parent_class($className));
  273. $thisMethods = array_diff($classMethods, $parentMethods);
  274. $out = [];
  275. foreach ($thisMethods as $method) {
  276. if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) {
  277. $out[] = $method;
  278. }
  279. }
  280. return $out;
  281. }
  282. /**
  283. * Generate the list of fixtures that will be required to run this test based on
  284. * loaded models.
  285. *
  286. * @param object $subject The object you want to generate fixtures for.
  287. * @return array Array of fixtures to be included in the test.
  288. */
  289. public function generateFixtureList($subject) {
  290. $this->_fixtures = [];
  291. if ($subject instanceof Table) {
  292. $this->_processModel($subject);
  293. } elseif ($subject instanceof Controller) {
  294. $this->_processController($subject);
  295. }
  296. return array_values($this->_fixtures);
  297. }
  298. /**
  299. * Process a model recursively and pull out all the
  300. * model names converting them to fixture names.
  301. *
  302. * @param Model $subject A Model class to scan for associations and pull fixtures off of.
  303. * @return void
  304. */
  305. protected function _processModel($subject) {
  306. $this->_addFixture($subject->alias());
  307. foreach ($subject->associations()->keys() as $alias) {
  308. $assoc = $subject->association($alias);
  309. $name = $assoc->target()->alias();
  310. if (!isset($this->_fixtures[$name])) {
  311. $this->_processModel($assoc->target());
  312. }
  313. if ($assoc->type() === Association::MANY_TO_MANY) {
  314. $junction = $assoc->junction();
  315. if (!isset($this->_fixtures[$junction->alias()])) {
  316. $this->_processModel($junction);
  317. }
  318. }
  319. }
  320. }
  321. /**
  322. * Process all the models attached to a controller
  323. * and generate a fixture list.
  324. *
  325. * @param \Cake\Controller\Controller $subject A controller to pull model names off of.
  326. * @return void
  327. */
  328. protected function _processController($subject) {
  329. $subject->constructClasses();
  330. $models = [$subject->modelClass];
  331. foreach ($models as $model) {
  332. list(, $model) = pluginSplit($model);
  333. $this->_processModel($subject->{$model});
  334. }
  335. }
  336. /**
  337. * Add class name to the fixture list.
  338. * Sets the app. or plugin.plugin_name. prefix.
  339. *
  340. * @param string $name Name of the Model class that a fixture might be required for.
  341. * @return void
  342. */
  343. protected function _addFixture($name) {
  344. if ($this->plugin) {
  345. $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.';
  346. } else {
  347. $prefix = 'app.';
  348. }
  349. $fixture = $prefix . $this->_fixtureName($name);
  350. $this->_fixtures[$name] = $fixture;
  351. }
  352. /**
  353. * Is a mock class required for this type of test?
  354. * Controllers require a mock class.
  355. *
  356. * @param string $type The type of object tests are being generated for eg. controller.
  357. * @return bool
  358. */
  359. public function hasMockClass($type) {
  360. $type = strtolower($type);
  361. return $type === 'controller';
  362. }
  363. /**
  364. * Generate a constructor code snippet for the type and class name
  365. *
  366. * @param string $type The Type of object you are generating tests for eg. controller
  367. * @param string $fullClassName The full classname of the class the test is being generated for.
  368. * @return array Constructor snippets for the thing you are building.
  369. */
  370. public function generateConstructor($type, $fullClassName) {
  371. list($namespace, $className) = namespaceSplit($fullClassName);
  372. $type = strtolower($type);
  373. $pre = $construct = $post = '';
  374. if ($type === 'table') {
  375. $className = str_replace('Table', '', $className);
  376. $pre = "\$config = TableRegistry::exists('{$className}') ? [] : ['className' => '{$fullClassName}'];\n";
  377. $construct = "TableRegistry::get('{$className}', \$config);\n";
  378. }
  379. if ($type === 'behavior' || $type === 'entity') {
  380. $construct = "new {$className}();\n";
  381. }
  382. if ($type === 'helper') {
  383. $pre = "\$view = new View();\n";
  384. $construct = "new {$className}(\$view);\n";
  385. }
  386. if ($type === 'component') {
  387. $pre = "\$registry = new ComponentRegistry();\n";
  388. $construct = "new {$className}(\$registry);\n";
  389. }
  390. if ($type === 'shell') {
  391. $pre = "\$this->io = \$this->getMock('Cake\Console\ConsoleIo');\n";
  392. $construct = "new {$className}(\$this->io);\n";
  393. }
  394. if ($type === 'cell') {
  395. $pre = "\$this->request = \$this->getMock('Cake\Network\Request');\n";
  396. $pre .= "\t\t\$this->response = \$this->getMock('Cake\Network\Response');\n";
  397. $construct = "new {$className}(\$this->request, \$this->response);\n";
  398. }
  399. return [$pre, $construct, $post];
  400. }
  401. /**
  402. * Generate the uses() calls for a type & class name
  403. *
  404. * @param string $type The Type of object you are generating tests for eg. controller
  405. * @param string $fullClassName The Classname of the class the test is being generated for.
  406. * @return array An array containing used classes
  407. */
  408. public function generateUses($type, $fullClassName) {
  409. $uses = [];
  410. $type = strtolower($type);
  411. if ($type === 'component') {
  412. $uses[] = 'Cake\Controller\ComponentRegistry';
  413. }
  414. if ($type === 'table') {
  415. $uses[] = 'Cake\ORM\TableRegistry';
  416. }
  417. if ($type === 'helper') {
  418. $uses[] = 'Cake\View\View';
  419. }
  420. $uses[] = $fullClassName;
  421. return $uses;
  422. }
  423. /**
  424. * Get the file path.
  425. *
  426. * @return string
  427. */
  428. public function getPath() {
  429. $dir = 'Test/TestCase/';
  430. $path = ROOT . DS . $dir;
  431. if (isset($this->plugin)) {
  432. $path = $this->_pluginPath($this->plugin) . $dir;
  433. }
  434. return $path;
  435. }
  436. /**
  437. * Make the filename for the test case. resolve the suffixes for controllers
  438. * and get the plugin path if needed.
  439. *
  440. * @param string $type The Type of object you are generating tests for eg. controller
  441. * @param string $className The fully qualified classname of the class the test is being generated for.
  442. * @return string filename the test should be created on.
  443. */
  444. public function testCaseFileName($type, $className) {
  445. $path = $this->getPath();
  446. $namespace = Configure::read('App.namespace');
  447. if ($this->plugin) {
  448. $namespace = Plugin::getNamespace($this->plugin);
  449. }
  450. $classTail = substr($className, strlen($namespace) + 1);
  451. $path = $path . $classTail . 'Test.php';
  452. return str_replace(['/', '\\'], DS, $path);
  453. }
  454. /**
  455. * Gets the option parser instance and configures it.
  456. *
  457. * @return \Cake\Console\ConsoleOptionParser
  458. */
  459. public function getOptionParser() {
  460. $parser = parent::getOptionParser();
  461. $parser->description(
  462. __d('cake_console', 'Bake test case skeletons for classes.')
  463. )->addArgument('type', [
  464. 'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'),
  465. 'choices' => [
  466. 'Controller', 'controller',
  467. 'Table', 'table',
  468. 'Entity', 'entity',
  469. 'Helper', 'helper',
  470. 'Component', 'component',
  471. 'Behavior', 'behavior'
  472. ]
  473. ])->addArgument('name', [
  474. 'help' => __d('cake_console', 'An existing class to bake tests for.')
  475. ])->addOption('fixtures', [
  476. 'help' => __d('cake_console', 'A comma separated list of fixture names you want to include.')
  477. ]);
  478. return $parser;
  479. }
  480. }