Plants.jsPlants.js is a JavaScript test runner intended for use with Node. It works well with Zombie.js.
Get it at github.com/alexyoung/plants.js
Installation
Installing the library with npm will make the plants command available:
npm install plants.js
Now run tests like this:
plants test/*.js
Usage
Tests are written in the CommonJS module style:
var assert = require('assert');
exports['test that tests run'] = function() {
assert.ok(true);
};
Setup and teardown is also supported:
var assert = require('assert'),
counter = 0;
// Setup should run first
exports['setup'] = function() {
assert.equal(0, counter);
};
exports['test that tests run'] = function() {
counter++;
assert.ok(true);
};
exports['teardown'] = function() {
assert.equal(1, counter);
};
Asynchronous Support
Some tests might take a while to run. The plants object is passed into each test, and it includes a defer method which can be used to wait until previous tests have finished:
var assert = require('assert'),
counter = 0;
// Setup should run first
exports['setup'] = function(plants) {
plants.defer(function(next) {
assert.equal(0, counter);
next();
});
};
exports['test asynchronously'] = function(plants) {
plants.defer(function(next) {
setTimeout(function() {
counter++;
assert.ok(true);
next();
}, 1000);
});
};
exports['teardown'] = function() {
assert.equal(1, counter);
};
| |
| lib/plants.js |
Plants.js
Copyright (C) 2011 Alex R. Young
MIT Licensed
|
var util = require('util'),
EventEmitter = require('events').EventEmitter,
_ = require('underscore'),
logger,
Tests,
printMessage,
colorize = true,
runner,
version = '0.0.1';
|
Print a message to the console.
param: String message The message to print param: String [className] The type of message: pass, fail param: String [prefix] A symbol that should appear before the message
|
printMessage = (function() {
function messageTypeToColor(messageType) {
switch (messageType) {
case 'pass':
return '32';
break;
case 'fail':
return '31';
break;
}
return '';
}
return function(message, messageType, prefix) {
var col = colorize ? messageTypeToColor(messageType) : false;
startCol = col ? '\033[' + col + 'm' : '',
endCol = col ? '\033[0m' : '',
console.log(startCol + (prefix ? prefix + ' ' : '') + message + endCol);
};
})();
logger = {
|
Display a message.
param: String message The message to print param: String [className] The type of message: pass, fail param: String [prefix] A symbol that should appear before the message
|
display: function(message, className, prefix) {
printMessage(message, className || 'trace', prefix || '');
},
|
Display an error.
|
error: function(message) {
logger.display(message, 'error', '\u2620');
},
|
Display a passed test with a message.
|
pass: function(message) {
logger.display(message, 'pass', '\u2713');
},
|
Display a failed test.
|
fail: function(message) {
logger.display(message, 'fail', '\u2715');
}
};
function TestRunner() {
this.files = [];
this.results = [];
this.passed = 0;
this.failed = 0;
this.errors = 0;
this.deferred = 0;
this.events = new EventEmitter();
this.testObject = null;
this.installEvents();
}
TestRunner.prototype = {
|
Generates a test result with a name and message.
|
Result: function(testName) {
return { name: testName, message: null };
},
|
Get a list of test names for the current testObject .
|
findTests: function() {
return _(this.testObject).chain()
.map(function(fn, name) {
if (/^test/i.test(name)) return name;
})
.compact()
.value();
},
|
Sets up the events required by tests.
|
installEvents: function() {
this.on('setup', _.bind(this.runSetup, this));
this.on('teardown', _.bind(this.runTeardown, this));
this.on('next', _.bind(this.runNext, this));
},
|
Runs the next test if there are no deferred functions.
|
nextIfNotDeferred: function() {
if (this.deferred === 0) this.emit('next');
},
|
Runs the next test.
|
runNext: function() {
if (!this.tests) return;
if (this.tests.length > 0) {
var testName = this.tests.shift();
this.run(testName);
this.nextIfNotDeferred();
} else {
this.emit('teardown');
}
},
|
Runs the setup function if it's present, then the next test.
|
runSetup: function() {
if (this.testObject.hasOwnProperty('setup'))
this.testObject.setup(this);
this.nextIfNotDeferred();
},
|
Runs the teardown function if it's present.
|
runTeardown: function() {
if (this.testObject.hasOwnProperty('teardown'))
this.testObject.teardown(this);
this.emit('end');
},
|
Sets the current testObject , then emits setup .
|
runTestObject: function(obj) {
this.testObject = obj;
this.tests = this.findTests();
this.emit('setup');
},
toString: function() {
return util.inspect(this);
},
|
Convenience accessor for the events object.
|
emit: function(name) {
this.events.emit(name);
},
|
Convenience accessor for the events object.
|
on: function() {
this.events.on.apply(this.events, arguments);
},
|
Runs a test in the current testObject , will call itself
recursively if the test is an object that contains sub-tests.
|
run: function(testName) {
this.deferred++;
var result = new this.Result(testName);
function showException(e) {
if (!!e.stack) {
logger.display(e.stack);
} else {
logger.display(e);
}
}
if (typeof this.testObject[testName] === 'object') {
logger.display('Running: ' + testName);
return this.runTestObject(this.testObject[testName]);
}
try {
this.testObject[testName](this);
this.passed += 1;
logger.pass(testName);
} catch (e) {
if (e.name === 'AssertionError') {
result.message = e.toString();
logger.fail('Assertion failed in: ' + testName);
showException(e);
this.failed += 1;
} else {
logger.error('Error in: ' + testName);
showException(e);
this.errors += 1;
}
} finally {
this.deferred--;
}
this.results.push(result);
},
|
Defers a function and passes in the current Plants object.
The passed-in function will get a method that must be called
to signal completion of an asynchronous operation.
|
defer: function(callback) {
this.deferred++;
callback(_.bind(function() {
this.deferred--;
if (this.deferred === 0) {
if (this.tests.length > 0) {
this.emit('next');
} else {
this.emit('teardown');
}
}
}, this));
},
|
Displays all test results.
|
report: function() {
logger.pass('Passed: ' + this.passed);
logger.fail('Failed: ' + this.failed);
logger.error('Errors: ' + this.errors);
process.exit(this.errors > 0 || this.failed > 0 ? 1 : 0);
},
|
Runs a file's tests.
param: String file The file name to run param: Object tests An object containing methods that start with 'test', and may include 'setup' and 'teardown'
|
runFile: function(file, tests) {
logger.display('Loaded file ' + file);
this.runTestObject(tests);
logger.display('');
},
|
Runs all tests.
|
runAll: function() {
if (this.deferred > 0) {
setTimeout(_.bind(function() { this.runAll(); }, this), 100);
} else if (this.files.length > 0) {
var file = this.files.shift();
try {
this.runFile(file, require(file));
this.runAll();
} catch (exception) {
error('Error in file: ' + file);
logger.display(exception);
logger.display('');
throw(exception);
}
} else if (this.files.length === 0 && this.deferred === 0) {
this.report();
}
}
};
runner = new TestRunner()
function error() {
runner.errors++;
return logger.error.apply(this, arguments);
};
|
Displays an error, available publicly.
|
exports.error = error;
|
Displays a message, available publicly.
|
exports.display = logger.display;
|
Runs tests, available publicly.
|
exports.run = function(files) {
runner.files = files;
runner.runAll();
};
exports.version = version;
|