├── examples └── express-zombie │ ├── views │ ├── index.jade │ ├── logged-in.jade │ ├── layout.jade │ └── login.jade │ ├── app.js │ └── test.js ├── .gitignore ├── Makefile ├── test ├── reporting.js ├── unit.js └── async.js ├── package.json ├── bin └── plants ├── LICENSE ├── docs ├── intro.md └── index.html ├── README.textile └── lib └── plants.js /examples/express-zombie/views/index.jade: -------------------------------------------------------------------------------- 1 | h1 Welcome 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.DS_Store 4 | *.pid 5 | tmp/ 6 | -------------------------------------------------------------------------------- /examples/express-zombie/views/logged-in.jade: -------------------------------------------------------------------------------- 1 | h1 Logged In 2 | 3 | -------------------------------------------------------------------------------- /examples/express-zombie/views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title Test 5 | body 6 | != body 7 | 8 | -------------------------------------------------------------------------------- /examples/express-zombie/views/login.jade: -------------------------------------------------------------------------------- 1 | form(method="POST", action="/login") 2 | input(type="text", name="username") 3 | input(type="text", name="password") 4 | input(type="submit", value="Log In") 5 | 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = bin/plants 2 | 3 | docs: 4 | dox \ 5 | --title "Plants.js" \ 6 | --intro docs/intro.md \ 7 | lib/*.js > docs/index.html 8 | 9 | test: 10 | @./$(BIN) test/*.js 11 | 12 | .PHONY: docs test 13 | -------------------------------------------------------------------------------- /test/reporting.js: -------------------------------------------------------------------------------- 1 | require.paths.unshift('lib/'); 2 | var assert = require('assert'); 3 | 4 | exports['test pass reporting'] = function() { 5 | assert.ok(true, 'This is a pass'); 6 | }; 7 | 8 | exports['test fail reporting'] = function() { 9 | assert.ok(false, 'This is a fail'); 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | require.paths.unshift('lib/'); 2 | var assert = require('assert'), 3 | counter = 0; 4 | 5 | // Setup should run first 6 | exports['setup'] = function() { 7 | assert.equal(0, counter); 8 | }; 9 | 10 | exports['test that tests run'] = function() { 11 | counter++; 12 | assert.ok(true); 13 | }; 14 | 15 | exports['teardown'] = function() { 16 | assert.equal(1, counter); 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plants.js", 3 | "description": "A Node test runner", 4 | "version": "0.0.1", 5 | "homepage": "https://github.com/alexyoung/plants.js", 6 | "author": "Alex R. Young (http://alexyoung.org)", 7 | "directories": { 8 | "lib": "./lib" 9 | }, 10 | "engines": { 11 | "node": ">= 0.2.0" 12 | }, 13 | "dependencies": { 14 | "underscore": ">= 1.0.3" 15 | }, 16 | "bin": { 17 | "plants": "bin/plants" 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | require.paths.unshift('lib/'); 2 | var assert = require('assert'), 3 | counter = 0; 4 | 5 | // Setup should run first 6 | exports['setup'] = function(plants) { 7 | plants.defer(function(next) { 8 | assert.equal(0, counter); 9 | next(); 10 | }); 11 | }; 12 | 13 | exports['test asynchronous events delay teardown'] = function(plants) { 14 | plants.defer(function(next) { 15 | setTimeout(function() { 16 | counter++; 17 | assert.ok(true); 18 | next(); 19 | }, 1000); 20 | }); 21 | }; 22 | 23 | exports['teardown'] = function() { 24 | assert.equal(1, counter); 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /examples/express-zombie/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express.createServer(); 3 | 4 | module.exports.app = app; 5 | app.set('view engine', 'jade'); 6 | app.use(express.bodyDecoder()); 7 | app.use(express.logger({ format: '\x1b[1m:method\x1b[0m \x1b[33m:url\x1b[0m :response-time ms' })); 8 | 9 | app.get('/', function(req, res) { 10 | res.render('index'); 11 | }); 12 | 13 | app.post('/login', function(req, res) { 14 | if (req.body.username === 'alex' 15 | && req.body.password === 'test') { 16 | res.render('logged-in'); 17 | } else { 18 | res.redirect('/'); 19 | } 20 | }); 21 | 22 | app.get('/login', function(req, res) { 23 | res.render('login'); 24 | }); 25 | 26 | if (!module.parent) { 27 | app.listen(3000); 28 | console.log('Express server listening on port %d, environment: %s', app.address().port, app.settings.env) 29 | } 30 | -------------------------------------------------------------------------------- /examples/express-zombie/test.js: -------------------------------------------------------------------------------- 1 | // run plants test.js 2 | 3 | var assert = require('assert'), 4 | zombie = require('zombie'), 5 | path = require('path'), 6 | app = require(path.join(__dirname, 'app')).app; 7 | 8 | app.listen(3001); 9 | 10 | exports['test index page'] = function(plants) { 11 | plants.defer(function(next) { 12 | zombie.visit('http://localhost:3001', function(err, browser, status) { 13 | assert.equal(browser.text('title'), 'Test'); 14 | next(); 15 | }); 16 | }); 17 | }; 18 | 19 | exports['test login'] = function(plants) { 20 | plants.defer(function(next) { 21 | zombie.visit('http://localhost:3001/login', function(err, browser, status) { 22 | browser. 23 | fill('username', 'alex'). 24 | fill('password', 'test'). 25 | pressButton('Log In', function(err, browser, status) { 26 | assert.equal(browser.text('h1'), 'Logged In'); 27 | next(); 28 | }); 29 | }); 30 | }); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /bin/plants: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var plants = require('../lib/plants'), 3 | path = require('path'), 4 | files = [], 5 | args = process.argv.slice(2); 6 | 7 | require.paths.unshift('.'); 8 | 9 | var usage = '' 10 | + 'Usage: plants [options] ' 11 | + '\n' 12 | + '\nOptions:' 13 | + '\n -v, --version Output version number' 14 | + '\n -h, --help Display help information' 15 | + '\n'; 16 | 17 | while (args.length) { 18 | var arg = args.shift(); 19 | switch (arg) { 20 | case '-h': 21 | case '--help': 22 | console.log(usage + '\n'); 23 | process.exit(1); 24 | break; 25 | case '-v': 26 | case '--version': 27 | console.log(plants.version); 28 | process.exit(1); 29 | break; 30 | default: 31 | if (/\.js$/.test(arg)) { 32 | files.push(arg); 33 | } 34 | break; 35 | } 36 | } 37 | 38 | function run() { 39 | if (!files.length) { 40 | plants.error('Please specify some files to test.'); 41 | process.exit(1); 42 | } 43 | 44 | plants.run(files); 45 | } 46 | 47 | run(files); 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Alex R. Young 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | Plants.js is a JavaScript test runner intended for use with Node. It works well with [Zombie.js](http://zombie.labnotes.org/). 2 | 3 | Get it at [github.com/alexyoung/plants.js](http://github.com/alexyoung/plants.js) 4 | 5 | ## Installation 6 | 7 | Installing the library with npm will make the `plants` command available: 8 | 9 | npm install plants.js 10 | 11 | Now run tests like this: 12 | 13 | plants test/*.js 14 | 15 | ## Usage 16 | 17 | Tests are written in the CommonJS module style: 18 | 19 | var assert = require('assert'); 20 | 21 | exports['test that tests run'] = function() { 22 | assert.ok(true); 23 | }; 24 | 25 | Setup and teardown is also supported: 26 | 27 | 28 | var assert = require('assert'), 29 | counter = 0; 30 | 31 | exports['setup'] = function() { 32 | assert.equal(0, counter); 33 | }; 34 | 35 | exports['test that tests run'] = function() { 36 | counter++; 37 | assert.ok(true); 38 | }; 39 | 40 | exports['teardown'] = function() { 41 | assert.equal(1, counter); 42 | }; 43 | 44 | ## Asynchronous Support 45 | 46 | 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: 47 | 48 | var assert = require('assert'), 49 | counter = 0; 50 | 51 | exports['setup'] = function(plants) { 52 | plants.defer(function(next) { 53 | assert.equal(0, counter); 54 | next(); 55 | }); 56 | }; 57 | 58 | exports['test asynchronously'] = function(plants) { 59 | plants.defer(function(next) { 60 | setTimeout(function() { 61 | counter++; 62 | assert.ok(true); 63 | next(); 64 | }, 1000); 65 | }); 66 | }; 67 | 68 | exports['teardown'] = function() { 69 | assert.equal(1, counter); 70 | }; 71 | 72 | 73 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 |
 2 |  @@@@@@@  @@@       @@@@@@  @@@  @@@ @@@@@@@  @@@@@@        @@@  @@@@@@
 3 |  @@!  @@@ @@!      @@!  @@@ @@!@!@@@   @@!   !@@            @@! !@@    
 4 |  @!@@!@!  @!!      @!@!@!@! @!@@!!@!   @!!    !@@!!         !!@  !@@!! 
 5 |  !!:      !!:      !!:  !!! !!:  !!!   !!:       !:!    .  .!!      !:!
 6 |   :       : ::.: :  :   : : ::    :     :    ::.: :  :: ::.::   ::.: : 
 7 |                                                  A Test Runner for Node
 8 |  Plants.js
 9 | 
10 | 11 | h3. News 12 | 13 | Twitter: "@alex_young":http://twitter.com/alex_young. 14 | 15 | * [2011-02-01] Project creation 16 | 17 | h3. About 18 | 19 | This is a test runner for Node, modelled on the "CommonJS Test Module":http://wiki.commonjs.org/wiki/Unit_Testing/1.0. 20 | 21 | Write tests as a CommonJS module, then export them with method names that start with test. Run plants test/file.js and Plants.js will run through each test and generate a report. 22 | 23 | An error code will be returned if any tests fail. 24 | 25 | h3. Installation 26 | 27 | npm install plants.js 28 | 29 | h3. Usage 30 | 31 | plants test/*.js 32 | 33 | h3. Advanced Usage 34 | 35 | Plants.js has a defer method for dealing with asynchronous tasks. The "documentation":http://alexyoung.github.com/plants.js has an example of this. 36 | 37 | h3. Documentation 38 | 39 | Documentation is available at "alexyoung.github.com/plants.js":http://alexyoung.github.com/plants.js. 40 | 41 | Examples can be found in examples/, including an Express app with Plants.js and Zombie.js tests. 42 | 43 | h3. To-do 44 | 45 | * Benchmarks 46 | * Assertion counts 47 | 48 | h3. License (MIT) 49 | 50 | Copyright (C) 2011 by Alex R. Young 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy 53 | of this software and associated documentation files (the "Software"), to deal 54 | in the Software without restriction, including without limitation the rights 55 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 56 | copies of the Software, and to permit persons to whom the Software is 57 | furnished to do so, subject to the following conditions: 58 | 59 | The above copyright notice and this permission notice shall be included in 60 | all copies or substantial portions of the Software. 61 | 62 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 63 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 64 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 65 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 66 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 67 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 68 | THE SOFTWARE. 69 | 70 | -------------------------------------------------------------------------------- /lib/plants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plants.js 3 | * Copyright (C) 2011 Alex R. Young 4 | * MIT Licensed 5 | */ 6 | 7 | var util = require('util'), 8 | EventEmitter = require('events').EventEmitter, 9 | _ = require('underscore'), 10 | logger, 11 | Tests, 12 | printMessage, 13 | colorize = true, 14 | runner, 15 | version = '0.0.1'; 16 | 17 | /** 18 | * Print a message to the console. 19 | * 20 | * @param {String} message The message to print 21 | * @param {String} [className] The type of message: pass, fail 22 | * @param {String} [prefix] A symbol that should appear before the message 23 | */ 24 | printMessage = (function() { 25 | function messageTypeToColor(messageType) { 26 | switch (messageType) { 27 | case 'pass': 28 | return '32'; 29 | break; 30 | 31 | case 'fail': 32 | return '31'; 33 | break; 34 | } 35 | 36 | return ''; 37 | } 38 | 39 | return function(message, messageType, prefix) { 40 | var col = colorize ? messageTypeToColor(messageType) : false; 41 | startCol = col ? '\033[' + col + 'm' : '', 42 | endCol = col ? '\033[0m' : '', 43 | console.log(startCol + (prefix ? prefix + ' ' : '') + message + endCol); 44 | }; 45 | })(); 46 | 47 | logger = { 48 | /** 49 | * Display a message. 50 | * 51 | * @param {String} message The message to print 52 | * @param {String} [className] The type of message: pass, fail 53 | * @param {String} [prefix] A symbol that should appear before the message 54 | */ 55 | display: function(message, className, prefix) { 56 | printMessage(message, className || 'trace', prefix || ''); 57 | }, 58 | 59 | /** 60 | * Display an error. 61 | * 62 | * @param {String} message The message to print 63 | */ 64 | error: function(message) { 65 | logger.display(message, 'error', '\u2620'); 66 | }, 67 | 68 | /** 69 | * Display a passed test with a message. 70 | * 71 | * @param {String} message The message to print 72 | */ 73 | pass: function(message) { 74 | logger.display(message, 'pass', '\u2713'); 75 | }, 76 | 77 | /** 78 | * Display a failed test. 79 | * 80 | * @param {String} message The message to print 81 | */ 82 | fail: function(message) { 83 | logger.display(message, 'fail', '\u2715'); 84 | } 85 | }; 86 | 87 | function TestRunner() { 88 | this.files = []; 89 | this.results = []; 90 | this.passed = 0; 91 | this.failed = 0; 92 | this.errors = 0; 93 | this.deferred = 0; 94 | this.events = new EventEmitter(); 95 | this.testObject = null; 96 | this.installEvents(); 97 | } 98 | 99 | TestRunner.prototype = { 100 | /** 101 | * Generates a test result with a name and message. 102 | * 103 | * @param {String} testName The test's name 104 | * @returns {Object} An object with a name and message 105 | */ 106 | Result: function(testName) { 107 | return { name: testName, message: null }; 108 | }, 109 | 110 | /** 111 | * Get a list of test names for the current `testObject`. 112 | * 113 | * @returns {Array} A list of test names 114 | */ 115 | findTests: function() { 116 | return _(this.testObject).chain() 117 | .map(function(fn, name) { 118 | if (/^test/i.test(name)) return name; 119 | }) 120 | .compact() 121 | .value(); 122 | }, 123 | 124 | /** 125 | * Sets up the events required by tests. 126 | */ 127 | installEvents: function() { 128 | this.on('setup', _.bind(this.runSetup, this)); 129 | this.on('teardown', _.bind(this.runTeardown, this)); 130 | this.on('next', _.bind(this.runNext, this)); 131 | }, 132 | 133 | /** 134 | * Runs the next test if there are no deferred functions. 135 | */ 136 | nextIfNotDeferred: function() { 137 | if (this.deferred === 0) this.emit('next'); 138 | }, 139 | 140 | /** 141 | * Runs the next test. 142 | */ 143 | runNext: function() { 144 | if (!this.tests) return; 145 | 146 | if (this.tests.length > 0) { 147 | var testName = this.tests.shift(); 148 | this.run(testName); 149 | this.nextIfNotDeferred(); 150 | } else { 151 | this.emit('teardown'); 152 | } 153 | }, 154 | 155 | /** 156 | * Runs the `setup` function if it's present, then the next test. 157 | */ 158 | runSetup: function() { 159 | if (this.testObject.hasOwnProperty('setup')) 160 | this.testObject.setup(this); 161 | this.nextIfNotDeferred(); 162 | }, 163 | 164 | /** 165 | * Runs the `teardown` function if it's present. 166 | */ 167 | runTeardown: function() { 168 | if (this.testObject.hasOwnProperty('teardown')) 169 | this.testObject.teardown(this); 170 | this.emit('end'); 171 | }, 172 | 173 | /** 174 | * Sets the current `testObject`, then emits `setup`. 175 | */ 176 | runTestObject: function(obj) { 177 | this.testObject = obj; 178 | this.tests = this.findTests(); 179 | this.emit('setup'); 180 | }, 181 | 182 | toString: function() { 183 | return util.inspect(this); 184 | }, 185 | 186 | /** 187 | * Convenience accessor for the events object. 188 | */ 189 | emit: function(name) { 190 | this.events.emit(name); 191 | }, 192 | 193 | /** 194 | * Convenience accessor for the events object. 195 | */ 196 | on: function() { 197 | this.events.on.apply(this.events, arguments); 198 | }, 199 | 200 | /** 201 | * Runs a test in the current `testObject`, will call itself 202 | * recursively if the test is an object that contains sub-tests. 203 | * 204 | * @param {String} testName The name of the test 205 | */ 206 | run: function(testName) { 207 | this.deferred++; 208 | var result = new this.Result(testName); 209 | 210 | function showException(e) { 211 | if (!!e.stack) { 212 | logger.display(e.stack); 213 | } else { 214 | logger.display(e); 215 | } 216 | } 217 | 218 | if (typeof this.testObject[testName] === 'object') { 219 | logger.display('Running: ' + testName); 220 | return this.runTestObject(this.testObject[testName]); 221 | } 222 | 223 | try { 224 | this.testObject[testName](this); 225 | this.passed += 1; 226 | logger.pass(testName); 227 | } catch (e) { 228 | if (e.name === 'AssertionError') { 229 | result.message = e.toString(); 230 | logger.fail('Assertion failed in: ' + testName); 231 | showException(e); 232 | this.failed += 1; 233 | } else { 234 | logger.error('Error in: ' + testName); 235 | showException(e); 236 | this.errors += 1; 237 | } 238 | } finally { 239 | this.deferred--; 240 | } 241 | 242 | this.results.push(result); 243 | }, 244 | 245 | /** 246 | * Defers a function and passes in the current Plants object. 247 | * The passed-in function will get a method that must be called 248 | * to signal completion of an asynchronous operation. 249 | * 250 | * @param {Function} callback A function that may not finish sequentially 251 | */ 252 | defer: function(callback) { 253 | this.deferred++; 254 | callback(_.bind(function() { 255 | this.deferred--; 256 | 257 | if (this.deferred === 0) { 258 | if (this.tests.length > 0) { 259 | this.emit('next'); 260 | } else { 261 | this.emit('teardown'); 262 | } 263 | } 264 | }, this)); 265 | }, 266 | 267 | /** 268 | * Displays all test results. 269 | */ 270 | report: function() { 271 | logger.pass('Passed: ' + this.passed); 272 | logger.fail('Failed: ' + this.failed); 273 | logger.error('Errors: ' + this.errors); 274 | process.exit(this.errors > 0 || this.failed > 0 ? 1 : 0); 275 | }, 276 | 277 | /** 278 | * Runs a file's tests. 279 | * 280 | * @param {String} file The file name to run 281 | * @param {Object} tests An object containing methods that start with 'test', and may include 'setup' and 'teardown' 282 | */ 283 | runFile: function(file, tests) { 284 | logger.display('Loaded file ' + file); 285 | this.runTestObject(tests); 286 | logger.display(''); 287 | }, 288 | 289 | /** 290 | * Runs all tests. 291 | */ 292 | runAll: function() { 293 | if (this.deferred > 0) { 294 | setTimeout(_.bind(function() { this.runAll(); }, this), 100); 295 | } else if (this.files.length > 0) { 296 | var file = this.files.shift(); 297 | try { 298 | this.runFile(file, require(file)); 299 | this.runAll(); 300 | } catch (exception) { 301 | error('Error in file: ' + file); 302 | logger.display(exception); 303 | logger.display(''); 304 | throw(exception); 305 | } 306 | } else if (this.files.length === 0 && this.deferred === 0) { 307 | this.report(); 308 | } 309 | } 310 | }; 311 | 312 | runner = new TestRunner() 313 | 314 | function error() { 315 | runner.errors++; 316 | return logger.error.apply(this, arguments); 317 | }; 318 | 319 | /** 320 | * Displays an error, available publicly. 321 | * 322 | * @param {String} message A message to display 323 | */ 324 | exports.error = error; 325 | 326 | /** 327 | * Displays a message, available publicly. 328 | * 329 | * @param {String} message A message to display 330 | */ 331 | exports.display = logger.display; 332 | 333 | /** 334 | * Runs tests, available publicly. 335 | * 336 | * @param {String} message A message to display 337 | */ 338 | exports.run = function(files) { 339 | runner.files = files; 340 | runner.runAll(); 341 | }; 342 | 343 | exports.version = version; 344 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Plants.js 4 | 5 | 99 | 109 | 110 | 111 | 180 | 186 | 197 | 198 | 199 | 206 | 232 | 233 | 234 | 241 | 246 | 247 | 248 | 255 | 260 | 261 | 262 | 269 | 274 | 275 | 276 | 283 | 303 | 304 | 305 | 312 | 317 | 318 | 319 | 326 | 336 | 337 | 338 | 342 | 349 | 350 | 351 | 355 | 360 | 361 | 362 | 366 | 379 | 380 | 381 | 385 | 392 | 393 | 394 | 398 | 405 | 406 | 407 | 411 | 422 | 423 | 424 | 428 | 433 | 434 | 435 | 439 | 444 | 445 | 446 | 454 | 494 | 495 | 496 | 505 | 521 | 522 | 523 | 527 | 535 | 536 | 537 | 544 | 551 | 552 | 553 | 557 | 585 | 586 | 587 | 594 | 597 | 598 | 599 | 606 | 609 | 610 | 611 | 618 | 627 | 628 |

Plants.js

Plants.js is a JavaScript test runner intended for use with Node. It works well with Zombie.js.

112 | 113 |

Get it at github.com/alexyoung/plants.js

114 | 115 |

Installation

116 | 117 |

Installing the library with npm will make the plants command available:

118 | 119 |
  npm install plants.js
120 | 121 |

Now run tests like this:

122 | 123 |
  plants test/*.js
124 | 125 |

Usage

126 | 127 |

Tests are written in the CommonJS module style:

128 | 129 |
  var assert = require('assert');
130 | 
131 |   exports['test that tests run'] = function() {
132 |     assert.ok(true);
133 |   };
134 | 135 |

Setup and teardown is also supported:

136 | 137 |
  var assert = require('assert'),
138 |       counter = 0;
139 | 
140 |   exports['setup'] = function() {
141 |     assert.equal(0, counter);
142 |   };
143 | 
144 |   exports['test that tests run'] = function() {
145 |     counter++;
146 |     assert.ok(true);
147 |   };
148 | 
149 |   exports['teardown'] = function() {
150 |     assert.equal(1, counter);
151 |   };
152 | 153 |

Asynchronous Support

154 | 155 |

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:

156 | 157 |
  var assert = require('assert'),
158 |       counter = 0;
159 | 
160 |   exports['setup'] = function(plants) {
161 |     plants.defer(function(next) {
162 |       assert.equal(0, counter);
163 |       next();
164 |     });
165 |   };
166 | 
167 |   exports['test asynchronously'] = function(plants) {
168 |     plants.defer(function(next) {
169 |       setTimeout(function() {
170 |         counter++;
171 |         assert.ok(true);
172 |         next();
173 |       }, 1000);
174 |     });
175 |   };
176 | 
177 |   exports['teardown'] = function() {
178 |     assert.equal(1, counter);
179 |   };

plants

lib/plants.js
181 |

Plants.js 182 | Copyright (C) 2011 Alex R. Young 183 | MIT Licensed 184 |

185 |
187 |
var util = require('util'),
188 |     EventEmitter = require('events').EventEmitter,
189 |     _ = require('underscore'),
190 |     logger,
191 |     Tests,
192 |     printMessage,
193 |     colorize = true,
194 |     runner,
195 |     version = '0.0.1';
196 |
200 |

Print a message to the console.

201 | 202 |

203 | 204 |
  • 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

205 |
207 |
printMessage = (function() {
208 |   function messageTypeToColor(messageType) {
209 |     switch (messageType) {
210 |       case 'pass':
211 |         return '32';
212 |       break;
213 | 
214 |       case 'fail':
215 |         return '31';
216 |       break;
217 |     }
218 | 
219 |     return '';
220 |   }
221 | 
222 |   return function(message, messageType, prefix) {
223 |     var col      = colorize ? messageTypeToColor(messageType) : false;
224 |         startCol = col ? '\033[' + col + 'm' : '',
225 |         endCol   = col ? '\033[0m' : '',
226 |     console.log(startCol + (prefix ? prefix + ' ' : '') + message + endCol);
227 |   };
228 | })();
229 | 
230 | logger = {
231 |
235 |

Display a message.

236 | 237 |

238 | 239 |
  • 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

240 |
242 |
display: function(message, className, prefix) {
243 |     printMessage(message, className || 'trace', prefix || '');
244 |   },
245 |
249 |

Display an error.

250 | 251 |

252 | 253 |
  • param: String message The message to print

254 |
256 |
error: function(message) {
257 |     logger.display(message, 'error', '\u2620');
258 |   },
259 |
263 |

Display a passed test with a message.

264 | 265 |

266 | 267 |
  • param: String message The message to print

268 |
270 |
pass: function(message) {
271 |     logger.display(message, 'pass', '\u2713');
272 |   },
273 |
277 |

Display a failed test.

278 | 279 |

280 | 281 |
  • param: String message The message to print

282 |
284 |
fail: function(message) {
285 |     logger.display(message, 'fail', '\u2715');
286 |   }
287 | };
288 | 
289 | function TestRunner() {
290 |   this.files = [];
291 |   this.results = [];
292 |   this.passed = 0;
293 |   this.failed = 0;
294 |   this.errors = 0;
295 |   this.deferred = 0;
296 |   this.events = new EventEmitter();
297 |   this.testObject = null;
298 |   this.installEvents();
299 | }
300 | 
301 | TestRunner.prototype = {
302 |
306 |

Generates a test result with a name and message.

307 | 308 |

309 | 310 |
  • param: String testName The test's name

  • returns: Object An object with a name and message

311 |
313 |
Result: function(testName) {
314 |     return { name: testName, message: null };
315 |   },
316 |
320 |

Get a list of test names for the current testObject.

321 | 322 |

323 | 324 |
  • returns: Array A list of test names

325 |
327 |
findTests: function() {
328 |     return _(this.testObject).chain()
329 |       .map(function(fn, name) {
330 |         if (/^test/i.test(name)) return name;
331 |       })
332 |       .compact()
333 |       .value();
334 |   },
335 |
339 |

Sets up the events required by tests. 340 |

341 |
343 |
installEvents: function() {
344 |     this.on('setup', _.bind(this.runSetup, this));
345 |     this.on('teardown', _.bind(this.runTeardown, this));
346 |     this.on('next', _.bind(this.runNext, this));
347 |   },
348 |
352 |

Runs the next test if there are no deferred functions. 353 |

354 |
356 |
nextIfNotDeferred: function() {
357 |     if (this.deferred === 0) this.emit('next');
358 |   },
359 |
363 |

Runs the next test. 364 |

365 |
367 |
runNext: function() {
368 |     if (!this.tests) return;
369 | 
370 |     if (this.tests.length > 0) {
371 |       var testName = this.tests.shift();
372 |       this.run(testName);
373 |       this.nextIfNotDeferred();
374 |     } else {
375 |       this.emit('teardown');
376 |     }
377 |   },
378 |
382 |

Runs the setup function if it's present, then the next test. 383 |

384 |
386 |
runSetup: function() {
387 |     if (this.testObject.hasOwnProperty('setup'))
388 |       this.testObject.setup(this);
389 |     this.nextIfNotDeferred();
390 |   },
391 |
395 |

Runs the teardown function if it's present. 396 |

397 |
399 |
runTeardown: function() {
400 |     if (this.testObject.hasOwnProperty('teardown'))
401 |       this.testObject.teardown(this);
402 |     this.emit('end');
403 |   },
404 |
408 |

Sets the current testObject, then emits setup. 409 |

410 |
412 |
runTestObject: function(obj) {
413 |     this.testObject = obj;
414 |     this.tests = this.findTests();
415 |     this.emit('setup');
416 |   },
417 | 
418 |   toString: function() {
419 |     return util.inspect(this);
420 |   },
421 |
425 |

Convenience accessor for the events object. 426 |

427 |
429 |
emit: function(name) {
430 |     this.events.emit(name);
431 |   },
432 |
436 |

Convenience accessor for the events object. 437 |

438 |
440 |
on: function() {
441 |     this.events.on.apply(this.events, arguments);
442 |   },
443 |
447 |

Runs a test in the current testObject, will call itself 448 | recursively if the test is an object that contains sub-tests.

449 | 450 |

451 | 452 |
  • param: String testName The name of the test

453 |
455 |
run: function(testName) {
456 |     this.deferred++;
457 |     var result = new this.Result(testName);
458 | 
459 |     function showException(e) {
460 |       if (!!e.stack) {
461 |         logger.display(e.stack);
462 |       } else {
463 |         logger.display(e);
464 |       }
465 |     }
466 | 
467 |     if (typeof this.testObject[testName] === 'object') {
468 |       logger.display('Running: ' + testName);
469 |       return this.runTestObject(this.testObject[testName]);
470 |     }
471 | 
472 |     try {
473 |       this.testObject[testName](this);
474 |       this.passed += 1;
475 |       logger.pass(testName);
476 |     } catch (e) {
477 |       if (e.name === 'AssertionError') {
478 |         result.message = e.toString();
479 |         logger.fail('Assertion failed in: ' + testName);
480 |         showException(e);
481 |         this.failed += 1;
482 |       } else {
483 |         logger.error('Error in: ' + testName);
484 |         showException(e);
485 |         this.errors += 1;
486 |       }
487 |     } finally {
488 |       this.deferred--;
489 |     }
490 | 
491 |     this.results.push(result);
492 |   },
493 |
497 |

Defers a function and passes in the current Plants object. 498 | The passed-in function will get a method that must be called 499 | to signal completion of an asynchronous operation.

500 | 501 |

502 | 503 |
  • param: Function callback A function that may not finish sequentially

504 |
506 |
defer: function(callback) {
507 |     this.deferred++;
508 |     callback(_.bind(function() {
509 |       this.deferred--;
510 | 
511 |       if (this.deferred === 0) {
512 |         if (this.tests.length > 0) {
513 |           this.emit('next');
514 |         } else {
515 |           this.emit('teardown');
516 |         }
517 |       }
518 |     }, this));
519 |   },
520 |
524 |

Displays all test results. 525 |

526 |
528 |
report: function() {
529 |     logger.pass('Passed: ' + this.passed);
530 |     logger.fail('Failed: ' + this.failed);
531 |     logger.error('Errors: ' + this.errors);
532 |     process.exit(this.errors > 0 || this.failed > 0 ? 1 : 0);
533 |   },
534 |
538 |

Runs a file's tests.

539 | 540 |

541 | 542 |
  • 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'

543 |
545 |
runFile: function(file, tests) {
546 |     logger.display('Loaded file ' + file);
547 |     this.runTestObject(tests);
548 |     logger.display('');
549 |   },
550 |
554 |

Runs all tests. 555 |

556 |
558 |
runAll: function() {
559 |     if (this.deferred > 0) {
560 |       setTimeout(_.bind(function() { this.runAll(); }, this), 100);
561 |     } else if (this.files.length > 0) {
562 |       var file = this.files.shift();
563 |       try {
564 |         this.runFile(file, require(file));
565 |         this.runAll();
566 |       } catch (exception) {
567 |         error('Error in file: ' + file);
568 |         logger.display(exception);
569 |         logger.display('');
570 |         throw(exception);
571 |       }
572 |     } else if (this.files.length === 0 && this.deferred === 0) {
573 |       this.report();
574 |     }
575 |   }
576 | };
577 | 
578 | runner = new TestRunner()
579 | 
580 | function error() {
581 |   runner.errors++;
582 |   return logger.error.apply(this, arguments);
583 | };
584 |
588 |

Displays an error, available publicly.

589 | 590 |

591 | 592 |
  • param: String message A message to display

593 |
595 |
exports.error = error;
596 |
600 |

Displays a message, available publicly.

601 | 602 |

603 | 604 |
  • param: String message A message to display

605 |
607 |
exports.display = logger.display;
608 |
612 |

Runs tests, available publicly.

613 | 614 |

615 | 616 |
  • param: String message A message to display

617 |
619 |
exports.run = function(files) {
620 |   runner.files = files;
621 |   runner.runAll();
622 | };
623 | 
624 | exports.version = version;
625 | 
626 |
--------------------------------------------------------------------------------