Testify.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <?php
  2. namespace Testify;
  3. /**
  4. * Testify - a micro unit testing framework
  5. *
  6. * This is the main class of the framework. Use it like this:
  7. *
  8. * @version 0.4.1
  9. * @author Martin Angelov
  10. * @author Marc-Olivier Fiset
  11. * @author Fabien Salathe
  12. * @link marco
  13. * @throws TestifyException
  14. * @license GPL
  15. */
  16. class Testify {
  17. private $tests = array();
  18. private $stack = array();
  19. private $fileCache = array();
  20. private $currentTestCase;
  21. private $suiteTitle;
  22. private $suiteResults;
  23. private $before = null;
  24. private $after = null;
  25. private $beforeEach = null;
  26. private $afterEach = null;
  27. /**
  28. * A public object for storing state and other variables across test cases and method calls.
  29. *
  30. * @var \StdClass
  31. */
  32. public $data = null;
  33. /**
  34. * The constructor.
  35. *
  36. * @param string $title The suite title
  37. */
  38. public function __construct($title)
  39. {
  40. $this->suiteTitle = $title;
  41. $this->data = new \StdClass;
  42. $this->suiteResults = array('pass' => 0, 'fail' => 0);
  43. }
  44. /**
  45. * Add a test case.
  46. *
  47. * @param string $name Title of the test case
  48. * @param \function $testCase (optional) The test case as a callback
  49. *
  50. * @return $this
  51. */
  52. public function test($name, \Closure $testCase = null)
  53. {
  54. if (is_callable($name)) {
  55. $testCase = $name;
  56. $name = "Test Case #" . (count($this->tests) + 1);
  57. }
  58. $this->affirmCallable($testCase, "test");
  59. $this->tests[] = array("name" => $name, "testCase" => $testCase);
  60. return $this;
  61. }
  62. /**
  63. * Executed once before the test cases are run.
  64. *
  65. * @param \function $callback An anonymous callback function
  66. */
  67. public function before(\Closure $callback)
  68. {
  69. $this->affirmCallable($callback, "before");
  70. $this->before = $callback;
  71. }
  72. /**
  73. * Executed once after the test cases are run.
  74. *
  75. * @param \function $callback An anonymous callback function
  76. */
  77. public function after(\Closure $callback)
  78. {
  79. $this->affirmCallable($callback, "after");
  80. $this->after = $callback;
  81. }
  82. /**
  83. * Executed for every test case, before it is run.
  84. *
  85. * @param \function $callback An anonymous callback function
  86. */
  87. public function beforeEach(\Closure $callback)
  88. {
  89. $this->affirmCallable($callback, "beforeEach");
  90. $this->beforeEach = $callback;
  91. }
  92. /**
  93. * Executed for every test case, after it is run.
  94. *
  95. * @param \function $callback An anonymous callback function
  96. */
  97. public function afterEach(\Closure $callback)
  98. {
  99. $this->affirmCallable($callback, "afterEach");
  100. $this->afterEach = $callback;
  101. }
  102. /**
  103. * Run all the tests and before / after functions. Calls {@see report} to generate the HTML report page.
  104. *
  105. * @return $this
  106. */
  107. public function run()
  108. {
  109. $arr = array($this);
  110. if (is_callable($this->before)) {
  111. call_user_func_array($this->before, $arr);
  112. }
  113. foreach($this->tests as $test) {
  114. $this->currentTestCase = $test['name'];
  115. if (is_callable($this->beforeEach)) {
  116. call_user_func_array($this->beforeEach, $arr);
  117. }
  118. // Executing the testcase
  119. call_user_func_array($test['testCase'], $arr);
  120. if (is_callable($this->afterEach)) {
  121. call_user_func_array($this->afterEach, $arr);
  122. }
  123. }
  124. if (is_callable($this->after)) {
  125. call_user_func_array($this->after, $arr);
  126. }
  127. $this->report();
  128. return $this;
  129. }
  130. /**
  131. * Alias for {@see assertTrue} method.
  132. *
  133. * @param boolean $arg The result of a boolean expression
  134. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  135. * @see Testify->assertTrue()
  136. *
  137. * @return boolean
  138. */
  139. public function assert($arg, $message = '')
  140. {
  141. return $this->assertTrue($arg, $message);
  142. }
  143. /**
  144. * Passes if given a truthfull expression.
  145. *
  146. * @param boolean $arg The result of a boolean expression
  147. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  148. *
  149. * @return boolean
  150. */
  151. public function assertTrue($arg, $message = '')
  152. {
  153. return $this->recordTest($arg == true, $message);
  154. }
  155. /**
  156. * Passes if given a falsy expression.
  157. *
  158. * @param boolean $arg The result of a boolean expression
  159. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  160. *
  161. * @return boolean
  162. */
  163. public function assertFalse($arg, $message = '')
  164. {
  165. return $this->recordTest($arg == false, $message);
  166. }
  167. /**
  168. * Passes if $arg1 == $arg2.
  169. *
  170. * @param mixed $arg1
  171. * @param mixed $arg2
  172. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  173. *
  174. * @return boolean
  175. */
  176. public function assertEquals($arg1, $arg2, $message = '')
  177. {
  178. return $this->recordTest($arg1 == $arg2, $message);
  179. }
  180. /**
  181. * Passes if $arg1 != $arg2.
  182. *
  183. * @param mixed $arg1
  184. * @param mixed $arg2
  185. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  186. *
  187. * @return boolean
  188. */
  189. public function assertNotEquals($arg1, $arg2, $message = '')
  190. {
  191. return $this->recordTest($arg1 != $arg2, $message);
  192. }
  193. /**
  194. * Passes if $arg1 === $arg2.
  195. *
  196. * @param mixed $arg1
  197. * @param mixed $arg2
  198. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  199. *
  200. * @return boolean
  201. */
  202. public function assertSame($arg1, $arg2, $message = '')
  203. {
  204. return $this->recordTest($arg1 === $arg2, $message);
  205. }
  206. /**
  207. * Passes if $arg1 !== $arg2.
  208. *
  209. * @param mixed $arg1
  210. * @param mixed $arg2
  211. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  212. *
  213. * @return boolean
  214. */
  215. public function assertNotSame($arg1, $arg2, $message = '')
  216. {
  217. return $this->recordTest($arg1 !== $arg2, $message);
  218. }
  219. /**
  220. * Passes if $arg is an element of $arr.
  221. *
  222. * @param mixed $arg
  223. * @param array $arr
  224. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  225. *
  226. * @return boolean
  227. */
  228. public function assertInArray($arg, array $arr, $message = '')
  229. {
  230. return $this->recordTest(in_array($arg, $arr), $message);
  231. }
  232. /**
  233. * Passes if $arg is not an element of $arr.
  234. *
  235. * @param mixed $arg
  236. * @param array $arr
  237. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  238. *
  239. * @return boolean
  240. */
  241. public function assertNotInArray($arg, array $arr, $message = '')
  242. {
  243. return $this->recordTest(!in_array($arg, $arr), $message);
  244. }
  245. /**
  246. * Unconditional pass.
  247. *
  248. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  249. *
  250. * @return boolean
  251. */
  252. public function pass($message = '')
  253. {
  254. return $this->recordTest(true, $message);
  255. }
  256. /**
  257. * Unconditional fail.
  258. *
  259. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  260. *
  261. * @return boolean
  262. */
  263. public function fail($message = '')
  264. {
  265. // This check fails every time
  266. return $this->recordTest(false, $message);
  267. }
  268. /**
  269. * Generates a pretty CLI or HTML5 report of the test suite status. Called implicitly by {@see run}.
  270. *
  271. * @return $this
  272. */
  273. public function report()
  274. {
  275. $title = $this->suiteTitle;
  276. $suiteResults = $this->suiteResults;
  277. $cases = $this->stack;
  278. if (php_sapi_name() === 'cli') {
  279. include dirname(__FILE__) . '/testify.report.cli.php';
  280. } else {
  281. include dirname(__FILE__) . '/testify.report.html.php';
  282. }
  283. return $this;
  284. }
  285. /**
  286. * A helper method for recording the results of the assertions in the internal stack.
  287. *
  288. * @param boolean $pass If equals true, the test has passed, otherwise failed
  289. * @param string $message (optional) Custom message
  290. *
  291. * @return boolean
  292. */
  293. private function recordTest($pass, $message = '')
  294. {
  295. if (!array_key_exists($this->currentTestCase, $this->stack) ||
  296. !is_array($this->stack[$this->currentTestCase])) {
  297. $this->stack[$this->currentTestCase]['tests'] = array();
  298. $this->stack[$this->currentTestCase]['pass'] = 0;
  299. $this->stack[$this->currentTestCase]['fail'] = 0;
  300. }
  301. $bt = debug_backtrace();
  302. $source = $this->getFileLine($bt[1]['file'], $bt[1]['line'] - 1);
  303. $bt[1]['file'] = basename($bt[1]['file']);
  304. $result = $pass ? "pass" : "fail";
  305. $this->stack[$this->currentTestCase]['tests'][] = array(
  306. "name" => $message,
  307. "type" => $bt[1]['function'],
  308. "result" => $result,
  309. "line" => $bt[1]['line'],
  310. "file" => $bt[1]['file'],
  311. "source" => $source
  312. );
  313. $this->stack[$this->currentTestCase][$result]++;
  314. $this->suiteResults[$result]++;
  315. return $pass;
  316. }
  317. /**
  318. * Internal method for fetching a specific line of a text file. With caching.
  319. *
  320. * @param string $file The file name
  321. * @param integer $line The line number to return
  322. *
  323. * @return string
  324. */
  325. private function getFileLine($file, $line)
  326. {
  327. if (!array_key_exists($file, $this->fileCache)) {
  328. $this->fileCache[$file] = file($file);
  329. }
  330. return trim($this->fileCache[$file][$line]);
  331. }
  332. /**
  333. * Internal helper method for determine whether a variable is callable as a function.
  334. *
  335. * @param mixed $callback The variable to check
  336. * @param string $name Used for the error message text to indicate the name of the parent context
  337. * @throws TestifyException if callback argument is not a function
  338. */
  339. private function affirmCallable(&$callback, $name)
  340. {
  341. if (!is_callable($callback)) {
  342. throw new TestifyException("$name(): is not a valid callback function!");
  343. }
  344. }
  345. /**
  346. * Alias for {@see assertEquals}.
  347. *
  348. * @deprecated Not recommended, use {@see assertEquals}
  349. * @param mixed $arg1
  350. * @param mixed $arg2
  351. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  352. *
  353. * @return boolean
  354. */
  355. public function assertEqual($arg1, $arg2, $message = '')
  356. {
  357. return $this->assertEquals($arg1, $arg2, $message);
  358. }
  359. /**
  360. * Alias for {@see assertSame}.
  361. *
  362. * @deprecated Not recommended, use {@see assertSame}
  363. * @param mixed $arg1
  364. * @param mixed $arg2
  365. * @param string $message (optional) Custom message. SHOULD be specified for easier debugging
  366. *
  367. * @return boolean
  368. */
  369. public function assertIdentical($arg1, $arg2, $message = '')
  370. {
  371. return $this->recordTest($arg1 === $arg2, $message);
  372. }
  373. /**
  374. * Alias for {@see run} method.
  375. *
  376. * @see Testify->run()
  377. *
  378. * @return $this
  379. */
  380. public function __invoke()
  381. {
  382. return $this->run();
  383. }
  384. }
  385. /**
  386. * TestifyException class
  387. *
  388. */
  389. class TestifyException extends \Exception
  390. {
  391. }