├── .gitignore ├── test.js ├── package.json ├── assert.js ├── README.md └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // for compatibility with http://wiki.commonjs.org/wiki/Unit_Testing/1.0#Test 2 | exports.run = require("./runner").run; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patr", 3 | "author": "Kris Zyp", 4 | "description": "Promise-based asynchronous test runner", 5 | "version": "0.2.6", 6 | "contributors": [], 7 | "keywords": [ 8 | "test", 9 | "promise" 10 | ], 11 | "licenses": [ 12 | { 13 | "type": "AFLv2.1", 14 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 15 | }, 16 | { 17 | "type": "BSD", 18 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 19 | } 20 | ], 21 | "repository": { 22 | "type":"git", 23 | "url":"http://github.com/kriszyp/patr" 24 | }, 25 | "mappings": { 26 | "promised-io": "http://github.com/kriszyp/promised-io/zipball/v0.2.3" 27 | }, 28 | "githubName": "patr", 29 | "type": "zip", 30 | "location": "http://github.com/kriszyp/patr/zipball/master", 31 | "dependencies": { 32 | "promised-io": ">0.2.3" 33 | }, 34 | "directories": { "lib": "." }, 35 | "main": "./runner", 36 | "icon": "http://packages.dojofoundation.org/images/persvr.png" 37 | } 38 | -------------------------------------------------------------------------------- /assert.js: -------------------------------------------------------------------------------- 1 | // Based on http://wiki.commonjs.org/wiki/Unit_Testing/1.0#Assert with 2 | // modifications to handle promises 3 | (function(define){ 4 | define(["promised-io/promise"],function(promise){ 5 | var assert; 6 | try{ 7 | assert = require("assert"); 8 | }catch(e){ 9 | assert = { 10 | equal: function(a, b, message){ 11 | console.assert(a == b, message); 12 | }, 13 | deepEqual: function(a, b, message){ 14 | if(a && typeof a == "object" && b && typeof b == "object"){ 15 | for(var i in a){ 16 | assert.deepEqual(a[i], b[i], message); 17 | } 18 | for(var i in b){ 19 | if(!(i in a)){ 20 | assert.equal(a[i], b[i], message); 21 | } 22 | } 23 | }else{ 24 | console.assert(a == b, message); 25 | } 26 | }, 27 | } 28 | } 29 | var exports = {}; 30 | var when = promise.when; 31 | exports.assert = {}; 32 | for(var key in assert){ 33 | exports[key] = assert[key]; 34 | } 35 | exports.equal = function(actual, expected, message) { 36 | if (typeof message === "undefined") { 37 | message = "got " + actual + ", expected " + expected; 38 | } 39 | return assert.equal(actual, expected, message); 40 | }; 41 | 42 | exports["throws"] = function(block, error, message){ 43 | var failed = false; 44 | try{ 45 | return when(block(), function(){ 46 | failed = true; 47 | throw new Error(message || ("Error" || error.name) + " was expected to be thrown and was not"); 48 | }, function(e){ 49 | if(error && !(e instanceof error)){ 50 | throw e; 51 | } 52 | }); 53 | }catch(e){ 54 | if((error && !(e instanceof error)) || failed){ 55 | throw e; 56 | } 57 | } 58 | }; 59 | return exports; 60 | }); 61 | })(typeof define!="undefined"?define:function(deps, factory){module.exports = factory.apply(this, deps.map(require));}); 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Promised-based Asynchronous Test Runner (patr) is a very simple, easy-to-use test 2 | runner that support asynchronous JavaScript testing with promises. Patr is based on 3 | the premise that testing should be as simple as creating an object with 4 | methods to perform tests. Objects can be nested to create subgroups of tests. 5 | Patr relies on the system's "assert" module for making assertions. An example test: 6 | 7 | var assert = require("assert"); 8 | require("patr/runner").run({testMath: function(){ 9 | assert.equal(3, Math.min(3, 5)); 10 | }); 11 | 12 | The suggested pattern for writing test files is to define the tests on the 13 | exports object and running the test runner if the module is the main module. 14 | This allows for direct execution of test files and easy inclusion of tests 15 | into other test groups. For example, we could define "my-math-test.js": 16 | 17 | var assert = require("assert"); 18 | exports.testMath = function(){ 19 | assert.equal(3, Math.min(3, 5)); 20 | }; 21 | 22 | if(require.main == module) 23 | require("patr/runner").run(exports); 24 | 25 | Now we can directly execute "my-math-test.js" or we could include it in another 26 | test file: 27 | 28 | exports.mathTests = require("my-math-test"); 29 | exports.otherTests = require("other-tests"); 30 | 31 | if(require.main == module) 32 | require("patr/runner").run(exports); 33 | 34 | With these aggregate test module, each test file's tests are included in a nested object 35 | that will be tested as a subgroup of tests. 36 | 37 | Using Promises for Asynchronous Testing 38 | ========================== 39 | 40 | Promises make asynchronous testing very simple. You simply return a promise from 41 | your test to indicate when a test is completed. When using promise-based coding this 42 | is super simple. For example, to test the contents of file loaded asynchronously using 43 | promised-io's fs module: 44 | 45 | var fs = require("promised-io/fs"); 46 | 47 | exports.testFile = function(){ 48 | return fs.readFile("testfile").then(function(contents){ 49 | assert.equal(contents.toString(), "expected contents"); 50 | }); 51 | }; 52 | ... 53 | 54 | Asynchronous assert module 55 | ====================== 56 | 57 | Patr includes an "assert" module (patr/assert) that is upgraded for promise-based asynchronous 58 | code blocks. In particular, the "throws" method can be used to enforce that a code block 59 | will eventually throw (or reject) even if it happens asynchronously. For example: 60 | 61 | var assert = require("patr/assert"); 62 | exports.testFile = function(){ 63 | return assert.throws(function(){ 64 | // asserts that this must throw/reject eventually 65 | return fs.readFile("non-existent file"); 66 | }); 67 | }; 68 | 69 | 70 | Advanced Testing 71 | ============ 72 | 73 | You can include additional testing options by setting flags on the test objects (that have the test functions). 74 | The test object can have the following properties: 75 | 76 | * iterations - The number of times to execute the test 77 | 78 | These properties are inherited, if they are set by a parent object, than the children 79 | objects will inherit the behavior unlesss it is overriden. 80 | 81 | These properties can also be set from the command line. This is done by including 82 | -name value arguments when starting the test module. For example: 83 | 84 | nodules test.js -iterations 1000 85 | 86 | 87 | Patr is part of the Persevere project, and therefore is licensed under the 88 | AFL or BSD license. The Persevere project is administered under the Dojo foundation, 89 | and all contributions require a Dojo CLA. -------------------------------------------------------------------------------- /runner.js: -------------------------------------------------------------------------------- 1 | (function(define){ 2 | define(["promised-io/promise", "promised-io/process"],function(promise, processModule){ 3 | var when = promise.when, 4 | all = promise.all, 5 | print = processModule.print, 6 | onError; 7 | function run(tests, args){ 8 | if(!args){ 9 | try{ 10 | var params = require("promised-io/process").args; 11 | args = {}; 12 | for(var i = 0; i < params.length; i++){ 13 | if(params[i].charAt(0) == "-"){ 14 | args[params[i].substring(1)] = params[i+1]; 15 | params++; 16 | } 17 | } 18 | }catch(e){} 19 | } 20 | print("Running tests "); 21 | doTests(compileTests(tests, args)); 22 | }; 23 | 24 | function doTests(tests, prefix){ 25 | prefix = prefix || ""; 26 | function doTest(index){ 27 | var done; 28 | try{ 29 | var test = tests[index++]; 30 | if(!test){ 31 | onError = false; 32 | return {failed: 0, total: tests.length}; 33 | } 34 | if(test.children){ 35 | print(prefix + "Group: " + test.name); 36 | return when(doTests(test.children, prefix + " "), function(childrenResults){ 37 | return when(doTest(index), function(results){ 38 | results.failed += childrenResults.failed; 39 | results.total += childrenResults.total - 1; 40 | return results; 41 | }); 42 | }); 43 | } 44 | onError = function(e){ 45 | print("onError"); 46 | testFailed(e); 47 | done = true; 48 | } 49 | var start = new Date().getTime(); 50 | var iterations = test.iterations || tests.flags.iterations; 51 | var concurrency = test.concurrency || tests.flags.concurrency || 1; 52 | var concurrentlyExecuting = 0; 53 | var iterationsLeft = iterations || 1; 54 | function runIteration(){ 55 | while(true){ 56 | iterationsLeft-- 57 | concurrentlyExecuting++; 58 | var result = test.test(); 59 | if(result instanceof Array){ 60 | result = all(result); 61 | } 62 | result = when(result, testCompleted, testFailed); 63 | if(iterationsLeft > 0){ 64 | if(concurrentlyExecuting >= concurrency){ 65 | return when(result, runIteration); 66 | } 67 | }else{ 68 | return result; 69 | } 70 | } 71 | 72 | } 73 | function testCompleted(){ 74 | concurrentlyExecuting--; 75 | if(!done){ 76 | if(iterationsLeft <= 0){ 77 | var duration = new Date().getTime() - start; 78 | print(prefix + test.name + ": passed" + (iterations || duration > 200 ? " in " + (duration / (iterations || 1)) + "ms per iteration" : "")); 79 | return doTest(index); 80 | } 81 | } 82 | } 83 | return runIteration(); 84 | }catch(e){ 85 | return testFailed(e); 86 | } 87 | function testFailed(e){ 88 | if(!done){ 89 | print(prefix + test.name + ": failed"); 90 | print(e.stack || e); 91 | return when(doTest(index), function(results){ results.failed++; return results;}); 92 | } 93 | } 94 | 95 | } 96 | return when(doTest(0), function(results){ 97 | print(prefix + "passed: " + (results.total - results.failed) + "/" + results.total); 98 | return results; 99 | }); 100 | } 101 | 102 | function compileTests(tests, parent){ 103 | var listOfTests = []; 104 | listOfTests.flags = {}; 105 | for(var i in parent){ 106 | listOfTests.flags[i] = parent[i]; 107 | } 108 | for(var i in tests){ 109 | listOfTests.flags[i] = tests[i]; 110 | } 111 | for(var i in tests){ 112 | if(i.substring(0,4) == "test"){ 113 | var test = tests[i]; 114 | if(typeof test == "function"){ 115 | listOfTests.push({ 116 | name: i, 117 | test: test 118 | }); 119 | } 120 | if(typeof test == "object"){ 121 | if(typeof test.runTest == "function"){ 122 | test.test = test.runTest; 123 | test.name = test.name || i; 124 | listOfTests.push(test); 125 | }else{ 126 | listOfTests.push({ 127 | name: i, 128 | children: compileTests(test, listOfTests.flags) 129 | }); 130 | } 131 | } 132 | } 133 | } 134 | return listOfTests; 135 | } 136 | 137 | if(typeof process !== "undefined"){ 138 | process.on("uncaughtException", function(e){ 139 | if(onError){ 140 | onError(e); 141 | }else{ 142 | print("Error thrown outside of test, unable to associate with a test. Ensure that a promise returned from a test is not fulfilled until the test is completed. Error: " + e.stack) 143 | } 144 | }); 145 | } 146 | 147 | 148 | return run.run = run; 149 | }); 150 | })(typeof define!="undefined"?define:function(deps, factory){module.exports = factory.apply(this, deps.map(require));}); 151 | --------------------------------------------------------------------------------