/* appjet:version 0.1 */ /* appjet:library */ // Copyright (c) 2009, Herbert Vojčík // Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php) /** * @overview This is a library that runs unit tests * and prints the results. It is stripped-down port * of existing jsunit 2.2 test runner. So far, it only has * html rendering of test results in a table. * It honours setUp and tearDown functions and treats * any function that begins with "test" as test function. * You can write setUp, tearDown and testXxx function * with prepended underscore (to ignore them in docs). * The runner works with both alternatives, though, * it's behaviour is undefined if you mix both forms * for the same function name. * * @example im port("lib-jsunit22"); function _testXxx() { ... } ... import("lib-testrunner"); if (you want auto redirection) { redirectTestRunnerResults(); } if (you want footer link) { setTestRunnerFooterLink(); } */ _runner = { start: function () { this._clear(); var timeRunStarted = new Date(); this._runAllTests(); this.timeTaken = (new Date() - timeRunStarted) / 1000; }, _clear: function() { this.totalCount = 0; this.errorCount = 0; this.failureCount = 0; this.results = []; }, _runAllTests: function () { this.addResults(new _suite().runAllTests()); }, addResults: function (suiteRawResults) { var self = this; suiteRawResults.forEach(function(rawResult) { var result = { name: rawResult.test.name, time: rawResult.time, status: "S", message: "passed", altName: rawResult.test.testName }; self.totalCount++; if (rawResult.exception != null) { if (_isTestException(rawResult.exception)) { result.status = "F"; self.failureCount++; } else { result.status = "E"; self.errorCount++; } result.message = _problemDetailMessageFor(rawResult.exception); } self.results.push(result); }); } }; function _problemDetailMessageFor(excep) { var result; if (_isTestException(excep)) { result = ''; if (excep.comment != null) result += ('"' + excep.comment + '"\n'); result += excep.testMessage; if (excep.stackTrace) result += '\n\nStack trace follows:\n' + excep.stackTrace; } else { result = 'Error message is:\n"'; result += (typeof(excep.description) == 'undefined') ? excep : excep.description; result += '"'; if (typeof(excep.stack) != 'undefined') // Mozilla only result += '\n\nStack trace follows:\n' + excep.stack; } return result; } function _suite() { this.tests = [].concat(page.testSuite); this.setup = _find(this.tests, "setUp") || function() {}; this.teardown = _find(this.tests, "tearDown") || function() {}; } function _find(array, name) { var index = -1; // array.every(function(value, i) { if (value.name === name) { index = i; return false; } return true; }); array.some(function(value, i) { if (value.name === name) { index = i; return true; } return false; }); if (index !== -1) { return array.splice(index, 1)[0]; } } _suite.prototype = { runAllTests: function() { var self = this; return this.tests.map(function(test) { return self.executeTestFunction(test); }); }, executeTestFunction: function (test) { var excep; var timeBefore = new Date(); try { this.setup(); test(); } catch (e1) { excep = e1; } try { this.teardown(); } catch (e2) { //Unlike JUnit, only assign a tearDown exception to excep if there is not already an exception from the test body if (!excep) { excep = e2; } } var time = (new Date() - timeBefore) / 1000; return {test:test, time:time, exception: excep}; }, executeUnsafeByName: function (name) { var run = false; var self = this; this.tests.forEach(function (test) { if (test.name === name) { run = true; self.setup(); test(); self.teardown(); } }); if (!run) { throw new Error("Test function <" + name + "> not found."); } } }; function _isTestException(exception) { return exception.isTestException === true; } /** * Runs the tests and fills the results. * You don not need to call it directly, * it is called when path is /testrunner/results. */ function runTheTests() { _runner.start(); } /** * This function renders test results, in normal html. * If argument is true, failed tests are rendered with * links to /testrunner/test?RestOfTheName in status field. * It is called automatically when path is /testrunner/results. * @see get_testrunner_test */ function renderTestsAsHtml(links) { page.setTitle(appjet.appName+" Test Runner"); page.head.write(''); print(P( {className:"testresult"+( _runner.errorCount?"E":_runner.failureCount?"F":"S")}, _runner.totalCount, " test(s) run, ", _runner.errorCount, " error(s), ", _runner.failureCount, " failure(s)." )); var table = UL({className:"testresults"}); for (var ri in _runner.results) { var result = _runner.results[ri], testStatus = result.status; testMessage = ""; if (links && testStatus != "S") { testStatus = A({href:"/testrunner/test?"+result.name}, testStatus); testMessage = UL(LI(result.message)); } table.push(LI( SPAN({className:"testresult"+result.status},"[",testStatus,"]"), " ", result.altName || _nameToTestCase(result.name), testMessage )); } print(table); } function _nameToTestCase(name) { var n = name.replace(/([A-Z])/g, " $1").replace("_", ", ","g").replace(/ /g, " "); return n.charAt(0).toUpperCase()+n.substring(1); } var _testRunnerCss = "" + "table.testresults td, table.testresults th { border: 1px solid silver; padding-left: 1ex; padding-right: 1ex }\n" + ".testresultS { background-color: lightgreen }\n" + ".testresultF { background-color: yellow }\n" + ".testresultE { background-color: red }\n" + "td.testcase, td.testmessage { text-align: left }\n" + "td.teststatus { text-align: center }\n"; /** Overwrites default CSS used when rendering tests. */ function setTestRunnerCss(css) { _testRunnerCss = css; } if (appjet.isPreview) { setTestRunnerFooterLink(); } switch (request.path) { case '/testrunner/test': new _suite().executeUnsafeByName(request.query); response.stop(); break; case '/testrunner/results': runTheTests(); renderTestsAsHtml(true); response.stop(); break; } /** * Redirects from / to /testrunner/results or returns if path is different than /. */ function runTestsByDefault() { if (request.path === "/") { response.redirect("/testrunner/results"); } } /** * Puts footer link to testrunner results. * Called automatically in preview. It is safe to call it more times * (you can call it again without checking isPreview). */ function setTestRunnerFooterLink() { import({}, "lib-support/footer-links").setFooterLink("/testrunner/results", "Test Runner"); } /* appjet:server */ var _globalEx = new Error("setUp probably wasn't started"); function thr(x) { if (x) throw x; } page.testSuite = [ function setUp() { _globalEx = null; }, function tearDown() { thr(_globalEx); }, function so_NothingBadHappens_ThatsIt() { var ex = _globalEx; _globalEx = null; thr(ex); }, function errorIsRaised_InsideTest() { throw new Error("Intentional error"); }, function failureIsRaised_InsideTest() { error = new Error(); error.isTestException = true; error.testMessage = "Intentional failure"; throw error; }, function errorIsRaised_InTearDown() { _globalEx = new Error("Intentional error in tearDown"); }, function failureIsRaised_InTearDown() { _globalEx = new Error(); _globalEx.isTestException = true; _globalEx.testMessage = "Intentional failure in tearDown"; }, ]; import("lib-support/runner"); runTestsByDefault(); setTestRunnerFooterLink();