runner.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /* appjet:version 0.1 */
  2. /* appjet:library */
  3. // Copyright (c) 2009, Herbert Vojčík
  4. // Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
  5. /**
  6. * @overview This is a library that runs unit tests
  7. * and prints the results. It is stripped-down port
  8. * of existing jsunit 2.2 test runner. So far, it only has
  9. * html rendering of test results in a table.
  10. * It honours setUp and tearDown functions and treats
  11. * any function that begins with "test" as test function.
  12. * You can write setUp, tearDown and testXxx function
  13. * with prepended underscore (to ignore them in docs).
  14. * The runner works with both alternatives, though,
  15. * it's behaviour is undefined if you mix both forms
  16. * for the same function name.
  17. *
  18. * @example
  19. im port("lib-jsunit22");
  20. function _testXxx() { ... }
  21. ...
  22. import("lib-testrunner");
  23. if (you want auto redirection) { redirectTestRunnerResults(); }
  24. if (you want footer link) { setTestRunnerFooterLink(); }
  25. */
  26. _runner = {
  27. start: function () {
  28. this._clear();
  29. var timeRunStarted = new Date();
  30. this._runAllTests();
  31. this.timeTaken = (new Date() - timeRunStarted) / 1000;
  32. },
  33. _clear: function() {
  34. this.totalCount = 0;
  35. this.errorCount = 0;
  36. this.failureCount = 0;
  37. this.results = [];
  38. },
  39. _runAllTests: function () {
  40. this.addResults(new _suite().runAllTests());
  41. },
  42. addResults: function (suiteRawResults) {
  43. var self = this;
  44. suiteRawResults.forEach(function(rawResult) {
  45. var result = {
  46. name: rawResult.test.name,
  47. time: rawResult.time,
  48. status: "S",
  49. message: "passed",
  50. altName: rawResult.test.testName
  51. };
  52. self.totalCount++;
  53. if (rawResult.exception != null) {
  54. if (_isTestException(rawResult.exception)) {
  55. result.status = "F";
  56. self.failureCount++;
  57. } else {
  58. result.status = "E";
  59. self.errorCount++;
  60. }
  61. result.message = _problemDetailMessageFor(rawResult.exception);
  62. }
  63. self.results.push(result);
  64. });
  65. }
  66. };
  67. function _problemDetailMessageFor(excep) {
  68. var result;
  69. if (_isTestException(excep)) {
  70. result = '';
  71. if (excep.comment != null)
  72. result += ('"' + excep.comment + '"\n');
  73. result += excep.testMessage;
  74. if (excep.stackTrace)
  75. result += '\n\nStack trace follows:\n' + excep.stackTrace;
  76. } else {
  77. result = 'Error message is:\n"';
  78. result +=
  79. (typeof(excep.description) == 'undefined') ?
  80. excep : excep.description;
  81. result += '"';
  82. if (typeof(excep.stack) != 'undefined') // Mozilla only
  83. result += '\n\nStack trace follows:\n' + excep.stack;
  84. }
  85. return result;
  86. }
  87. function _suite() {
  88. this.tests = [].concat(page.testSuite);
  89. this.setup = _find(this.tests, "setUp") || function() {};
  90. this.teardown = _find(this.tests, "tearDown") || function() {};
  91. }
  92. function _find(array, name) {
  93. var index = -1;
  94. // array.every(function(value, i) { if (value.name === name) { index = i; return false; } return true; });
  95. array.some(function(value, i) { if (value.name === name) { index = i; return true; } return false; });
  96. if (index !== -1) { return array.splice(index, 1)[0]; }
  97. }
  98. _suite.prototype = {
  99. runAllTests: function() {
  100. var self = this;
  101. return this.tests.map(function(test) { return self.executeTestFunction(test); });
  102. },
  103. executeTestFunction: function (test) {
  104. var excep;
  105. var timeBefore = new Date();
  106. try {
  107. this.setup();
  108. test();
  109. }
  110. catch (e1) {
  111. excep = e1;
  112. }
  113. try {
  114. this.teardown();
  115. }
  116. catch (e2) {
  117. //Unlike JUnit, only assign a tearDown exception to excep if there is not already an exception from the test body
  118. if (!excep) { excep = e2; }
  119. }
  120. var time = (new Date() - timeBefore) / 1000;
  121. return {test:test, time:time, exception: excep};
  122. },
  123. executeUnsafeByName: function (name) {
  124. var run = false;
  125. var self = this;
  126. this.tests.forEach(function (test) {
  127. if (test.name === name) {
  128. run = true;
  129. self.setup();
  130. test();
  131. self.teardown();
  132. }
  133. });
  134. if (!run) { throw new Error("Test function <" + name + "> not found."); }
  135. }
  136. };
  137. function _isTestException(exception) {
  138. return exception.isTestException === true;
  139. }
  140. /**
  141. * Runs the tests and fills the results.
  142. * You don not need to call it directly,
  143. * it is called when path is /testrunner/results.
  144. */
  145. function runTheTests() {
  146. _runner.start();
  147. }
  148. /**
  149. * This function renders test results, in normal html.
  150. * If argument is true, failed tests are rendered with
  151. * links to /testrunner/test?RestOfTheName in status field.
  152. * It is called automatically when path is /testrunner/results.
  153. * @see get_testrunner_test
  154. */
  155. function renderTestsAsHtml(links) {
  156. page.setTitle(appjet.appName+" Test Runner");
  157. page.head.write('<style type="text/css">');
  158. page.head.write(_testRunnerCss);
  159. page.head.write('</style>');
  160. print(P(
  161. {className:"testresult"+(
  162. _runner.errorCount?"E":_runner.failureCount?"F":"S")},
  163. _runner.totalCount, " test(s) run, ",
  164. _runner.errorCount, " error(s), ",
  165. _runner.failureCount, " failure(s)."
  166. ));
  167. var table = UL({className:"testresults"});
  168. for (var ri in _runner.results) {
  169. var result = _runner.results[ri],
  170. testStatus = result.status;
  171. testMessage = "";
  172. if (links && testStatus != "S") {
  173. testStatus = A({href:"/testrunner/test?"+result.name}, testStatus);
  174. testMessage = UL(LI(result.message));
  175. }
  176. table.push(LI(
  177. SPAN({className:"testresult"+result.status},"[",testStatus,"]"),
  178. " ",
  179. result.altName || _nameToTestCase(result.name),
  180. testMessage
  181. ));
  182. }
  183. print(table);
  184. }
  185. function _nameToTestCase(name) {
  186. var n = name.replace(/([A-Z])/g, " $1").replace("_", ", ","g").replace(/ /g, " ");
  187. return n.charAt(0).toUpperCase()+n.substring(1);
  188. }
  189. var _testRunnerCss = "" +
  190. "table.testresults td, table.testresults th { border: 1px solid silver; padding-left: 1ex; padding-right: 1ex }\n" +
  191. ".testresultS { background-color: lightgreen }\n" +
  192. ".testresultF { background-color: yellow }\n" +
  193. ".testresultE { background-color: red }\n" +
  194. "td.testcase, td.testmessage { text-align: left }\n" +
  195. "td.teststatus { text-align: center }\n";
  196. /** Overwrites default CSS used when rendering tests. */
  197. function setTestRunnerCss(css) {
  198. _testRunnerCss = css;
  199. }
  200. if (appjet.isPreview) { setTestRunnerFooterLink(); }
  201. switch (request.path) {
  202. case '/testrunner/test':
  203. new _suite().executeUnsafeByName(request.query);
  204. response.stop();
  205. break;
  206. case '/testrunner/results':
  207. runTheTests();
  208. renderTestsAsHtml(true);
  209. response.stop();
  210. break;
  211. }
  212. /**
  213. * Redirects from / to /testrunner/results or returns if path is different than /.
  214. */
  215. function runTestsByDefault() {
  216. if (request.path === "/") { response.redirect("/testrunner/results"); }
  217. }
  218. /**
  219. * Puts footer link to testrunner results.
  220. * Called automatically in preview. It is safe to call it more times
  221. * (you can call it again without checking isPreview).
  222. */
  223. function setTestRunnerFooterLink() {
  224. import({}, "lib-support/footer-links").setFooterLink("/testrunner/results", "Test Runner");
  225. }
  226. /* appjet:server */
  227. var _globalEx = new Error("setUp probably wasn't started");
  228. function thr(x) { if (x) throw x; }
  229. page.testSuite = [
  230. function setUp() { _globalEx = null; },
  231. function tearDown() { thr(_globalEx); },
  232. function so_NothingBadHappens_ThatsIt()
  233. { var ex = _globalEx; _globalEx = null; thr(ex); },
  234. function errorIsRaised_InsideTest()
  235. { throw new Error("Intentional error"); },
  236. function failureIsRaised_InsideTest() {
  237. error = new Error();
  238. error.isTestException = true;
  239. error.testMessage = "Intentional failure";
  240. throw error;
  241. },
  242. function errorIsRaised_InTearDown()
  243. { _globalEx = new Error("Intentional error in tearDown"); },
  244. function failureIsRaised_InTearDown() {
  245. _globalEx = new Error();
  246. _globalEx.isTestException = true;
  247. _globalEx.testMessage = "Intentional failure in tearDown";
  248. },
  249. ];
  250. import("lib-support/runner");
  251. runTestsByDefault();
  252. setTestRunnerFooterLink();