TestTask.php 15 KB

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