Last active
August 29, 2015 14:26
-
-
Save markandrus/5f1956187e98b2007d6b to your computer and use it in GitHub Desktop.
We should have a concurrent Mocha-like test framework!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
var assert = require('assert'); | |
var util = require('util'); | |
var DEFAULT_TIMEOUT = 200; | |
// Abstract Syntax Tree for Defining Tests | |
// ---------------------------------------------------------------------------- | |
/** | |
* Construct a {@link Node}. | |
* @class | |
* @classdesc A {@link Node} is a node in the abstract syntax tree (AST) that | |
* defines our tests. | |
* @param {string} name - the name of the {@link Node} | |
* @param {?body} function - the function which defines the {@link Node} (for | |
* {@link Describe}s, this is invoked immediately; for {@link It}s, it is | |
* invoked during testing) | |
* @param {number=} timeout - the maximum amount of time to run any test | |
* defined by this {@link Node} (inherited from the parent {@link Node} | |
* unless overridden) | |
*/ | |
function Node(name, body, timeout) { | |
body = body || null; | |
Object.defineProperties(this, { | |
_body: { | |
enumerable: true, | |
get: function() { | |
return body; | |
}, | |
set: function(_body) { | |
body = _body; | |
} | |
}, | |
_name: { | |
enumerable: true, | |
value: name | |
}, | |
_timeout: { | |
enumerable: true, | |
get: function() { | |
return timeout; | |
}, | |
set: function(_timeout) { | |
timeout = _timeout; | |
} | |
} | |
}); | |
} | |
/** | |
* Get the tests described by this {@link Node}. | |
* @instance | |
* @private | |
* @param {?string} prefix - the test name prefix | |
* @param {?Array<function>} before - the functions to run before the test | |
* @param {?Array<function>} after - the functions to run after the test (if | |
* @param {number=} timeout - the maximum amount of time to run the tests | |
* if not specified (defaults to 200 milliseconds) | |
* @returns Array<Promise> | |
*/ | |
Node.prototype._getTests = function _getTests() { | |
throw new Error('Node#_getTests must be implemented'); | |
}; | |
/** | |
* Set the {@link Node}'s timeout. A falsy value disables the timeout. | |
* @instance | |
* @param {?timeout} timeout | |
* @returns Node | |
*/ | |
Node.prototype.timeout = function timeout(_timeout) { | |
_timeout = _timeout || 0; | |
this._timeout = _timeout; | |
return this; | |
}; | |
/** | |
* Construct a {@link Describe}. | |
* @class | |
* @classdesc A {@link Describe} defines zero or more tests. | |
* @extends Node | |
* @param {string} name - the name of this {@link Describe} | |
* @param {?function} body - the function which defines the child {@link Node}s | |
* of the {@link Describe} | |
* @param {number=} timeout - the maximum amount of time to run any test | |
* defined by this {@link Describe} (inherited from the parent {@link Node} | |
* unless overridden) | |
*/ | |
function Describe(name, body, timeout) { | |
Node.call(this, name, body, timeout); | |
var after = null; | |
var before = null; | |
Object.defineProperties(this, { | |
_after: { | |
enumerable: true, | |
get: function() { | |
return after; | |
}, | |
set: function(_after) { | |
after = _after; | |
} | |
}, | |
before: { | |
enumerable: true, | |
get: function() { | |
return before; | |
}, | |
set: function(before) { | |
before = before; | |
} | |
}, | |
_children: { | |
enumerable: true, | |
value: [] | |
} | |
}); | |
} | |
/* Here's a little hack for Mocha/Jasmine-style calls to `describe`, `it`, | |
`beforeEach`, etc.: keep a path to the Node we are working in. */ | |
var currentPath = []; | |
function focus(node) { | |
currentPath.unshift(node); | |
} | |
function unfocus() { | |
currentPath.shift(); | |
} | |
function getFocus() { | |
return currentPath[0] || null; | |
} | |
/** | |
* Construct a {@link Describe} and invoke its body function. | |
* @param {string} name - the name of the {@link Describe} | |
* @param {function} body - the function which defines the child {@link Node}s | |
* of the {@link Describe} | |
* @returns Describe | |
*/ | |
Describe.describe = function describe(name, body) { | |
var describe = getFocus(); | |
if (describe) { | |
return describe.describe(name, body); | |
} | |
describe = new Describe(name, body); | |
focus(describe); | |
describe._body.bind(describe)(); | |
return describe; | |
}; | |
Describe.after = Describe.afterEach; | |
Describe.afterEach = function afterEach(body) { | |
var describe = getFocus(); | |
if (!describe) { | |
throw new Error('You must call describe first'); | |
} | |
return describe.afterEach(body); | |
}; | |
Describe.before = Describe.beforeEach; | |
Describe.beforeEach = function beforeEach(body) { | |
var describe = getFocus(); | |
if (!describe) { | |
throw new Error('You must call describe first'); | |
} | |
return describe.beforeEach(body); | |
}; | |
Describe.it = function it(name, body) { | |
var describe = getFocus(); | |
if (!describe) { | |
throw new Error('You must call describe first'); | |
} | |
return describe.it(name, body); | |
}; | |
Describe.timeout = function timeout(_timeout) { | |
var describe = getFocus(); | |
if (!describe) { | |
throw new Error('You must call describe first'); | |
} | |
return describe.timeout(_timeout); | |
}; | |
util.inherits(Describe, Node); | |
/** | |
* Get the zero or more tests defined by this {@link Describe}. | |
* @instance | |
* @private | |
* @param {?string} prefix - the test name prefix | |
* @param {?Array<function>} before - the functions to run before the test | |
* @param {?Array<function>} after - the functions to run after the test (if | |
* successful) | |
* @param {number=} timeout - the maximum amount of time to run the tests | |
* if not specified (defaults to 200 milliseconds) | |
* @returns Array<Promise> | |
*/ | |
Describe.prototype._getTests = function _getTests(prefix, before, after, | |
timeout) | |
{ | |
prefix = prefix || ''; | |
before = before || []; | |
after = after || []; | |
prefix = prefix + ' ' + this._name; | |
before = this._before ? before.slice().concat([this._before]) : before; | |
after = this._after ? after.slice().concat([this._after]) : after; | |
timeout = this._timeout || timeout; | |
var tests = []; | |
return this._children.reduce(function(tests, node) { | |
return tests.concat(node._getTests(prefix, before, after, timeout)); | |
}.bind(this), []); | |
}; | |
Describe.prototype.after = Describe.prototype.afterEach; | |
Describe.prototype.afterEach = function afterEach(body) { | |
if (this._after) { | |
throw new Error('You may only call Describe#afterEach once'); | |
} | |
this._after = body; | |
return this; | |
}; | |
Describe.prototype.before = Describe.prototype.beforeEach; | |
Describe.prototype.beforeEach = function beforeEach(body) { | |
if (this.before) { | |
throw new Error('You may only call Describe#beforeEach once'); | |
} | |
this._before = body; | |
return this; | |
}; | |
/** | |
* Construct a child {@link Describe} and invoke its body function. Returns the | |
* parent {@link Describe}. | |
* @instance | |
* @param {string} name - the name of the {@link Describe} | |
* @param {function} body - the function which defines the child {@link Node}s | |
* of the {@link Describe} | |
* @returns Describe | |
*/ | |
Describe.prototype.describe = function describe(name, body) { | |
var describe = new Describe(name, body); | |
this._children.push(describe); | |
focus(describe); | |
describe._body.bind(describe)(); | |
unfocus(); | |
return this; | |
}; | |
/** | |
* Construct a child {@link It}. | |
* @instance | |
* @param {string} name - the name of the {@link It} | |
* @param {function} body - the function which defines the child {@link Node}s | |
* of the {@link Describe} | |
* @returns Describe | |
*/ | |
Describe.prototype.it = function it(name, body) { | |
var it = new It(name, body); | |
this._children.push(it); | |
return this; | |
}; | |
/** | |
* Construct an {@link It}. | |
* @class | |
* @classdesc An {@link It} defines a single test. | |
* @extends Node | |
* @param {string} name - the name of the {@link It} | |
* @param {function} body - the test function | |
* @param {number=} timeout - the maximum amount of time to run the test | |
* defined by this {@link It} (inherited from the parent {@link Node} | |
* unless overridden) | |
*/ | |
function It(name, body, timeout) { | |
Node.call(this, name, body, timeout); | |
} | |
util.inherits(It, Node); | |
/** | |
* Get the single test defined by this {@link It}. | |
* @instance | |
* @param {?string} prefix - the test name prefix | |
* @param {?Array<function>} before - the functions to run before the test | |
* @param {?Array<function>} after - the functions to run after the test (if | |
* successful) | |
* @param {number=} timeout - the maximum amount of time to run the test | |
* if not specified (defaults to 200 milliseconds) | |
* @returns Array<Promise> | |
*/ | |
It.prototype._getTests = function _getTests(prefix, before, after, timeout) { | |
prefix = prefix || ''; | |
before = before || []; | |
after = after || []; | |
timeout = this._timeout || timeout; | |
prefix = prefix + ' ' + this._name; | |
var self = this; | |
return [sequenceDoneables(before).then(function() { | |
return convertDoneableToPromise(self._body.bind(self)); | |
}).then(function() { | |
return sequenceDoneables(after); | |
}).then(function() { | |
return prefix + ': passed'; | |
}, function() { | |
return prefix + ': failed'; | |
}) | |
]; | |
}; | |
// Functions for working with Doneables | |
// ---------------------------------------------------------------------------- | |
function sequenceDoneables(doneables) { | |
if (!doneables.length) { | |
return Promise.resolve(); | |
} | |
var doneable = doneables[0]; | |
doneables = doneables.slice(1); | |
return convertDoneableToPromise(doneable).then(function() { | |
return sequenceDoneables(doneables); | |
}); | |
} | |
function convertDoneableToPromise(doneable) { | |
return new Promise(function(resolve, reject) { | |
function done(error) { | |
if (arguments.length) { | |
return reject(error); | |
} | |
resolve(); | |
} | |
if (doneable.length) { | |
return doneable(done); | |
} else { | |
doneable(); | |
resolve(); | |
} | |
}); | |
} | |
// Example Usage | |
// ---------------------------------------------------------------------------- | |
// Eventually, we'd define a Mocha-style test runner that pre-populates the | |
// following globals: | |
var afterEach = Describe.afterEach; | |
var beforeEach = Describe.beforeEach; | |
var describe = Describe.describe; | |
var it = Describe.it; | |
var timeout = Describe.timeout; | |
// Then, we could use `describe` and friends as normal. | |
var describeBlock = describe('Alpha', function() { | |
timeout(); | |
beforeEach(function(done) { | |
setTimeout(done, 5000); | |
}); | |
it('works', function() { | |
}); | |
describe('Beta', function() { | |
beforeEach(function(done) { | |
setTimeout(done, 5000); | |
}); | |
it('works', function() { | |
}); | |
it('does not work', function() { | |
assert(false); | |
}); | |
afterEach(function(done) { | |
setTimeout(done, 5000); | |
}); | |
}); | |
it('works asynchronously', function(done) { | |
done(); | |
}); | |
it('does not work asynchronously', function(done) { | |
done(false); | |
}); | |
afterEach(function(done) { | |
setTimeout(done, 5000); | |
}); | |
}); | |
// Except we can execute the Promises concurrently! | |
Promise.all(describeBlock._getTests()).then(function(results) { | |
console.log(results.join('\n')); | |
}); | |
// Exports | |
// ---------------------------------------------------------------------------- | |
module.exports = Describe; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Test time takes the expected 20 seconds:
Next step is to hook up to one of the test reporters from Mocha and do streaming updates.