dotest
Version:
Yet another unit test runner
977 lines (797 loc) • 20.8 kB
JavaScript
/*
Name: doTest - Unit tests runner
Description: Yet another unit test runner for Node.js
Author: Franklin van de Meent (https://frankl.in)
Source & docs: https://github.com/fvdm/nodejs-dotest
Feedback: https://github.com/fvdm/nodejs-dotest/issues
License: Unlicense (public domain, see LICENSE file)
*/
var path = require ('path');
var util = require ('util');
var dir = path.parse (process.mainModule.filename) .dir.replace (/\/lib$/, '');
var pkg = require (path.join (dir, 'package.json'));
var lib = require (path.join (__dirname, 'package.json'));
var testFunc;
var queue = [];
var next = -1;
var unitTests = {};
var onExitCallback;
var counters = {
fail: 0,
warn: 0,
startTime: Date.now ()
};
var config = {
wait: 0,
noConsole: false
};
/**
* ANSI colorize a string
*
* @param color {String} - The color to add
* @param str {String} - The string to alter
* @returns {String}
*/
function colorStr (color, str) {
var colors = {
red: '\u001b[31m',
green: '\u001b[32m',
yellow: '\u001b[33m',
blue: '\u001b[34m',
magenta: '\u001b[35m',
cyan: '\u001b[36m',
gray: '\u001b[37m',
bold: '\u001b[1m',
plain: '\u001b[0m'
};
return colors [color] + str + colors.plain;
}
/**
* console.log with style
*
* @param [type] {String=plain} - Formatting style
* @param str {String} - The string to alter
* @returns {void}
*/
function log (type, str) {
var types = {
good: ['green', 'good'],
info: ['cyan', 'info']
};
if (!str) {
str = type;
type = 'plain';
}
switch (type) {
case 'note':
console.log (colorStr ('bold', str));
break;
case 'fail':
counters.fail++;
console.log (colorStr ('red', 'FAIL') + ' ' + str);
break;
case 'warn':
counters.warn++;
console.log (colorStr ('yellow', 'warn') + ' ' + str);
break;
case 'error':
counters.fail++;
console.log (colorStr ('red', 'ERROR ') + str.message + '\n');
console.dir (str, {
depth: null,
colors: true
});
// node v6 includes stack trace in the Error
if (process.versions.node < '6.0.0') {
console.log ();
console.log (str.stack);
console.log ();
}
break;
case 'object':
console.dir (str, {
depth: null,
colors: true
});
break;
case 'plain': console.log (str); break;
default:
console.log (colorStr (types[type][0], types[type][1]) + ' ' + str);
break;
}
}
/**
* Run next test in queue
*
* @param index {number} - queue[] index
* @returns {void}
*/
function doNext (index) {
console.log (
'\n\n'
+ colorStr ('cyan', (index + 1) + '/' + queue.length)
+ ' '
+ colorStr ('bold', queue [index] .label)
);
console.log ();
queue [index] .runner (testFunc);
}
/**
* Run callback, optional wait time, run next test in queue
*
* @callback callback
* @param [callback] {function} - Run callback before next test
* @returns {void}
*/
function done (callback) {
if (callback instanceof Function) {
callback (next);
}
next++;
if (queue [next]) {
if (next && config.wait) {
setTimeout (
function () {
doNext (next);
},
config.wait
);
return;
}
doNext (next);
}
}
/**
* Get any var type
* The order of if's is important
*
* @param input {mixed} - The value to check
* @returns {string} - Lowercase type
*/
function getType (input) {
if (input instanceof Date) {
return 'date';
}
if (input instanceof RegExp) {
return 'regexp';
}
if (input instanceof Error) {
return 'error';
}
if (input instanceof Function) {
return 'function';
}
if (input instanceof Array) {
return 'array';
}
if (input instanceof Object) {
return 'object';
}
if (input === null) {
return 'null';
}
return (typeof input);
}
/**
* Get formatted var type for console
*
* @param str {string} - The var to convert
* @param [noType = false] {boolean} - Don't append ' (type)'
* @returns {string} - i.e. hello (string)
*/
function typeStr (str, noType) {
var type = getType (str);
var doType = !noType ? ' (' + type + ')' : '';
var typeMatch = type.match (/(string|boolean|number|date|regexp|array)/);
var length = '';
// length
switch (type) {
case 'string':
case 'array':
length = ' (' + str.length + ')';
break;
case 'object':
case 'error':
length = ' (' + Object.keys (str).length + ')';
break;
default:
length = '';
break;
}
// parse special
if (type.match (/(object|array)/)) {
str = util.inspect (str, {
depth: null,
colors: true
});
str = str.replace ('\n', ' ');
if (str.length <= 50) {
str = colorStr ('magenta', str[0])
+ str.slice (1, -1)
+ colorStr ('magenta', str.slice (-1))
+ doType;
return str;
}
str += '\u001b[0m';
}
// parse function
if (type === 'function') {
str = util.inspect (str, {
colors: true
});
str += '\u001b[0m';
return str;
}
// parse rest
str = String (str);
if (typeMatch && str.length && str.length <= 50) {
return colorStr ('magenta', str) + doType;
}
return colorStr ('magenta', type) + length;
}
/**
* Write test result to console
*
* @param level {string} - fail, warn
* @param what {string} - Text to prepend in blue
* @param result {object}
* @param result.state {boolean} - Check result
* @param result.data {mixed} - Check input
* @param describe {string, object} - Describe result, i.e. 'an Array'
* @param describe.true {string} - Override default describe if true
* @param describe.false {string} - Override default describe if false
* @returns {void}
*/
function output (level, what, result, describe) {
var state = (result.state === true) ? 'good' : level;
var typestrGood = typeStr (result.data, true);
var typestrFail = typeStr (result.data);
var str = '';
// log line
switch (state) {
case 'good': str = colorStr ('green', 'good'); break;
case 'fail': str = colorStr ('red', 'FAIL'); break;
case 'warn': str = colorStr ('yellow', 'warn'); break;
default:
// skip
break;
}
str += ' ' + colorStr ('blue', what) + ' ';
// describe result
if (result.state) {
str += describe.true || typestrGood + ' is ' + describe;
} else {
counters[level]++;
str += describe.false || typestrFail + ' should be ' + describe;
}
// output
console.log (str);
}
/**
* Handle process exit
*
* @param [fromMethod] {boolean} - Used internally to prevent double logs
* @param [code] {number} - Enforce exit status code if not fail
* @returns {void}
*/
function processExit (fromProcess, code) {
var timing = (Date.now () - counters.startTime) / 1000;
// library mode
if (config.noConsole) {
return;
}
// tester mode
if (fromProcess) {
console.log ('\n');
log ('info', colorStr ('yellow', counters.fail) + ' errors');
log ('info', colorStr ('yellow', counters.warn) + ' warnings');
console.log ();
log ('info', colorStr ('yellow', timing) + ' seconds');
console.log ();
}
if (counters.fail) {
process.exit (1);
} else {
process.exit (code || 0);
}
}
process.on ('exit', function (code) {
if (typeof onExitCallback === 'function') {
onExitCallback (code);
}
processExit (true, code);
});
/**
* Prevent errors from killing the process
*
* @param err {Error} - The error that occured
* @returns {void}
*/
function uncaughtException (err) {
log ('error', err);
}
process.on ('uncaughtException', uncaughtException);
/**
* Methods for test()
*/
function testLog (level, str) {
var typestr = typeStr (str);
var doDump = typestr.match (/(object|array)/) && typestr.match (/ \(\d+\)/);
if (typeof str === 'string') {
log (level, str);
return;
}
log (level, typestr);
if (doDump) {
log ('object', str);
}
}
unitTests = {
done: done,
error: function error (str) {
log ('error', str);
return unitTests;
},
good: function good (str) {
testLog ('good', str);
return unitTests;
},
fail: function fail (str) {
testLog ('fail', str);
return unitTests;
},
warn: function warn (str) {
testLog ('warn', str);
return unitTests;
},
info: function info (str) {
testLog ('info', str);
return unitTests;
},
exit: function exit () {
testLog ('info', 'Exit process');
processExit (false);
return unitTests;
}
};
/**
* Test for Error
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isError = function isError (level, what, input) {
var result = {
state: getType (input) === 'error',
data: input
};
output (level, what, result, 'an Error');
return unitTests;
};
/**
* Test for Object
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isObject = function isObject (level, what, input) {
var result = {
state: getType (input) === 'object',
data: input
};
output (level, what, result, 'an Object');
return unitTests;
};
/**
* Test for Array
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isArray = function isArray (level, what, input) {
var result = {
state: getType (input) === 'array',
data: input
};
output (level, what, result, 'an Array');
return unitTests;
};
/**
* Test for String
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test againstuncaughtException
* @returns {object} - unitTests
*/
unitTests.isString = function isString (level, what, input) {
var result = {
state: getType (input) === 'string',
data: input
};
output (level, what, result, 'a String');
return unitTests;
};
/**
* Test for Number
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isNumber = function isNumber (level, what, input) {
var result = {
state: getType (input) === 'number',
data: input
};
output (level, what, result, 'a Number');
return unitTests;
};
/**
* Test for Undefined
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isUndefined = function isUndefined (level, what, input) {
var result = {
state: getType (input) === 'undefined',
data: input
};
output (level, what, result, 'Undefined');
return unitTests;
};
/**
* Test for null
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isNull = function isNull (level, what, input) {
var result = {
state: input === null,
data: input
};
output (level, what, result, 'Null');
return unitTests;
};
/**
* Test for NaN
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isNaN = function isNan (level, what, input) {
var result = {
state: isNaN (input),
data: input
};
output (level, what, result, 'NaN');
return unitTests;
};
/**
* Test for Boolean
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isBoolean = function isBoolean (level, what, input) {
var result = {
state: getType (input) === 'boolean',
data: input
};
output (level, what, result, 'a Boolean');
return unitTests;
};
/**
* Test for Function
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isFunction = function isFunction (level, what, input) {
var result = {
state: getType (input) === 'function',
data: input
};
output (level, what, result, 'a Function');
return unitTests;
};
/**
* Test for Date
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isDate = function isDate (level, what, input) {
var result = {
state: getType (input) === 'date',
data: input
};
output (level, what, result, 'a Date');
return unitTests;
};
/**
* Check if two values are exactly the same
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param one {mixed} - variable to test against
* @param two {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isExactly = function isExactly (level, what, one, two) {
var typestrOne = typeStr (one);
var typestrTwo = typeStr (two);
var result = {
state: one === two,
data: two
};
var describe = {
true: 'is exactly ' + typestrTwo,
false: typestrOne + ' should be exactly ' + typestrTwo
};
output (level, what, result, describe);
return unitTests;
};
/**
* Check if two values are not the same
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param one {mixed} - variable to test against
* @param two {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isNot = function isNot (level, what, one, two) {
var typestrOne = typeStr (one);
var typestrTwo = typeStr (two);
var result = {
state: one !== two,
data: two
};
var describe = {
true: typestrOne + ' is not equal to ' + typestrTwo,
false: typestrOne + ' should not be equal to ' + typestrTwo
};
output (level, what, result, describe);
return unitTests;
};
/**
* Check if input is a RegExp
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isRegexp = function isRegexp (level, what, input) {
var result = {
state: getType (input) === 'regexp',
data: input
};
output (level, what, result, 'a RegExp');
return unitTests;
};
/**
* Check if a string matches a regex
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @param regex {mixed} - regular expression to match
* @returns {object} - unitTests
*/
unitTests.isRegexpMatch = function isRegexpMatch (level, what, input, regex) {
var typestrOne = typeStr (input);
var typestrTwo = typeStr (regex);
var result = {
state: !!~input.match (regex),
data: input
};
var describe = {
true: typestrOne + ' is matching ' + typestrTwo,
false: typestrOne + ' should be matching ' + typestrTwo
};
output (level, what, result, describe);
return unitTests;
};
/**
* Check if the two values meet the condition
*
* @param level {string} - fail, warn
* @param what {string} - describe the test
* @param one {mixed} - variable to test against
* @param operator {string} - < > <= >=
* @param two {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isCondition = function isCondition (level, what, one, operator, two) {
var typestrOne = typeStr (one);
var typestrTwo = typeStr (two);
var result = {
state: false,
data: two
};
var str = typestrOne + ' ' + colorStr ('yellow', operator) + ' ' + typestrTwo;
var describe = {
true: str,
false: str
};
switch (operator) {
case '<': result.state = one < two; break;
case '>': result.state = one > two; break;
case '<=': result.state = one <= two; break;
case '>=': result.state = one >= two; break;
default: result.state = false; break;
}
output (level, what, result, describe);
return unitTests;
};
/**
* Check if input is an empty var, string, object, array, error
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isEmpty = function isEmpty (level, what, input) {
var type = getType (input);
var result = {
state: false,
data: input
};
if (type === 'undefined') {
result.state = true;
} else if (input === null) {
result.state = true;
} else if (type === 'string' && !input) {
result.state = true;
} else if (type === 'object' && !Object.keys (input).length) {
result.state = true;
} else if (type === 'array' && !input.length) {
result.state = true;
} else if (type === 'error' && !Object.keys (input).length && !input.message) {
result.state = true;
}
output (level, what, result, 'Empty');
return unitTests;
};
/**
* Check if input is not an empty var, string, object, array, error
*
* @param level {string} - fail, warn
* @param what {string} - describe input data, i.e. 'data.sub'
* @param input {mixed} - variable to test against
* @returns {object} - unitTests
*/
unitTests.isNotEmpty = function isNotEmpty (level, what, input) {
var type = getType (input);
var result = {
state: true,
data: input
};
if (type === 'undefined') {
result.state = false;
} else if (input === null) {
result.state = false;
} else if (type === 'string' && !input) {
result.state = false;
} else if (type === 'object' && !Object.keys (input).length) {
result.state = false;
} else if (type === 'array' && !input.length) {
result.state = false;
} else if (type === 'error' && !Object.keys (input).length && !input.message) {
result.state = false;
}
output (level, what, result, 'not Empty');
return unitTests;
};
function test (err) {
if (err) {
log ('error', err);
}
return unitTests;
}
testFunc = test;
/**
* Start tests
*
* @param wait {number=0} - Wait time between tests, in ms (1000 = 1 sec)
* @returns {void}
*/
function run (wait) {
config.wait = process.env.DOTEST_WAIT || wait || 0;
if (!config.noConsole && next === -1) {
log ('note', 'Running tests...\n');
log ('note', 'Module name: ' + colorStr ('yellow', pkg.name));
log ('note', 'Module version: ' + colorStr ('yellow', pkg.version));
log ('note', 'Node.js version: ' + colorStr ('yellow', process.versions.node));
log ('note', 'dotest version: ' + colorStr ('yellow', lib.version));
if (pkg.bugs && pkg.bugs.url) {
console.log ();
log ('note', 'Module issues: ' + colorStr ('yellow', pkg.bugs.url));
}
}
done ();
}
/**
* Add a test to the queue
*
* @param label {string} - Text to describe test
* @param runner {function} - The function with test code, `function (test) { test().isObject(...); }`
* @returns {void}
*/
function add (label, runner) {
queue.push ({
label: label,
runner: runner
});
}
/**
* Set callback that runs when process exits
*
* @callcack callback
* @param callback {function} - `function (code) {}`
* @returns {void}
*/
function onExit (callback) {
onExitCallback = callback;
}
/**
* Change configuration
*
* @param name {object, string) - Config param or object
* @param [name.noConsole = false] {boolean} - Don't console.log anything
* @param [value] {string) - Param value if name is a string
* @returns config {object} - Current settings
*/
function setConfig (name, value) {
var key;
if (name instanceof Object) {
for (key in name) {
config [key] = name [key];
}
return config;
}
config [name] = value;
return config;
}
/**
* Module interface
*/
module.exports = {
package: pkg,
add: add,
run: run,
log: log,
test: test,
exit: unitTests.exit,
onExit: onExit,
colorStr: colorStr,
getType: getType,
config: setConfig,
get length () {
return queue.length;
}
};