├── .gitignore ├── docs ├── layout │ ├── foot.html │ └── head.html ├── index.md ├── index.html └── api.html ├── .gitmodules ├── test ├── match │ └── test.js ├── async.test.js ├── bar.test.js ├── .invalid.test.js ├── foo.test.js ├── earlyexit │ └── forever.test.js ├── serial │ ├── teardown.test.js │ ├── async.test.js │ └── http.test.js ├── this.test.js ├── earlyexit.test.js ├── match.test.js ├── local-assert.test.js ├── assert.test.js └── http.test.js ├── package.json ├── Makefile ├── Readme.md ├── History.md └── bin └── expresso /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib-cov 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /docs/layout/foot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/jscoverage"] 2 | path = deps/jscoverage 3 | url = git://github.com/visionmedia/node-jscoverage.git 4 | -------------------------------------------------------------------------------- /test/match/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | module.exports = { 4 | 'this test will pass': function() { 5 | assert.ok(true); 6 | }, 7 | 'this test will fail': function() { 8 | assert.ok(false); 9 | }, 10 | } -------------------------------------------------------------------------------- /test/async.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | 8 | setTimeout(function(){ 9 | exports['test async exports'] = function(){ 10 | assert.ok('wahoo'); 11 | }; 12 | }, 100); -------------------------------------------------------------------------------- /test/bar.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert') 7 | , bar = require('bar'); 8 | 9 | module.exports = { 10 | 'bar()': function(){ 11 | assert.equal('bar', bar.bar()); 12 | } 13 | }; -------------------------------------------------------------------------------- /test/.invalid.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file should never run because it begins with a . 3 | */ 4 | 5 | var assert = require('assert'); 6 | 7 | exports['failure'] = function() { 8 | assert.ok(false, "Don't run files beginning with a dot"); 9 | }; 10 | -------------------------------------------------------------------------------- /test/foo.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert') 7 | , foo = require('foo'); 8 | 9 | module.exports = { 10 | 'foo()': function(){ 11 | assert.equal('foo', foo.foo()); 12 | assert.equal('foo', foo.foo()); 13 | } 14 | }; -------------------------------------------------------------------------------- /test/earlyexit/forever.test.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var assert = require('assert'); 3 | var server = http.createServer(function(req, res) { /* Never send a response */ }); 4 | 5 | exports['assert.response'] = function() { 6 | // This will keep running for a while because the server never returns. 7 | assert.response(server, { url: '/' }, { status: 200 }); 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "expresso", 2 | "version": "0.9.2", 3 | "description": "TDD framework, light-weight, fast, CI-friendly", 4 | "author": "TJ Holowaychuk ", 5 | "repository": "git://github.com/visionmedia/expresso", 6 | "bin": { 7 | "expresso": "./bin/expresso", 8 | "node-jscoverage": "./deps/jscoverage/node-jscoverage" 9 | }, 10 | "scripts": { 11 | "preinstall": "make deps/jscoverage/node-jscoverage" 12 | } 13 | } -------------------------------------------------------------------------------- /test/serial/teardown.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | 8 | var a = 0; 9 | 10 | module.exports = { 11 | teardown : function (done) { 12 | a--; 13 | done(); 14 | }, 15 | test1 : function(done) { 16 | a++; 17 | assert.strictEqual(1, a); 18 | done(); 19 | }, 20 | test2 : function (done) { 21 | a++; 22 | assert.strictEqual(1, a); 23 | done(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/this.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This. 3 | */ 4 | 5 | module.exports = { 6 | 'test this': function(beforeExit, assert) { 7 | assert.equal(this.suite, 'this.test.js'); 8 | assert.equal(this.title, 'test this'); 9 | assert.ok(this.assert); 10 | 11 | var exiting = false; 12 | this.on('exit', function() { 13 | exiting = true; 14 | }); 15 | 16 | beforeExit(function() { 17 | assert.ok(exiting); 18 | }); 19 | } 20 | }; -------------------------------------------------------------------------------- /test/earlyexit.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var spawn = require('child_process').spawn; 3 | 4 | exports['early exit'] = function(beforeExit) { 5 | var completed = false; 6 | 7 | var proc = spawn('bin/expresso', ['test/earlyexit/forever.test.js', '--port', '23444']); 8 | proc.on('exit', function(code) { 9 | completed = true; 10 | assert.equal(1, code, "assert.response didn't report an error while still running"); 11 | }); 12 | 13 | setTimeout(function() { 14 | proc.kill('SIGINT'); 15 | }, 1000); 16 | 17 | // Also kill the child if it still exists. 18 | beforeExit(function() { 19 | proc.kill(); 20 | assert.ok(completed); 21 | }); 22 | }; -------------------------------------------------------------------------------- /test/serial/async.test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | , setup = 0 4 | , order = []; 5 | 6 | module.exports = { 7 | setup: function(done){ 8 | ++setup; 9 | done(); 10 | }, 11 | 12 | a: function(done){ 13 | assert.equal(1, setup); 14 | order.push('a'); 15 | setTimeout(function(){ 16 | done(); 17 | }, 500); 18 | }, 19 | 20 | b: function(done){ 21 | assert.equal(2, setup); 22 | order.push('b'); 23 | setTimeout(function(){ 24 | done(); 25 | }, 200); 26 | }, 27 | 28 | c: function(done){ 29 | assert.equal(3, setup); 30 | order.push('c'); 31 | setTimeout(function(){ 32 | done(); 33 | }, 1000); 34 | }, 35 | 36 | d: function(){ 37 | assert.eql(order, ['a', 'b', 'c']); 38 | } 39 | }; -------------------------------------------------------------------------------- /test/match.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var spawn = require('child_process').spawn; 3 | 4 | module.exports = { 5 | 'will run only matched tests': function() { 6 | var proc = spawn('bin/expresso', ['-m', 'p..s', 'test/match/test.js']); 7 | proc.on('exit', function(code) { 8 | completed = true; 9 | assert.equal(0, code, 'failing test was not filtered out'); 10 | }); 11 | setTimeout(function() { 12 | proc.kill('SIGINT'); 13 | }, 1000); 14 | }, 15 | 'will run tests matched in a subsequent expression': function() { 16 | var proc = spawn('bin/expresso', ['-m', 'nothing', '--match', 'p..s', 'test/match/test.js']); 17 | proc.on('exit', function(code) { 18 | completed = true; 19 | assert.equal(0, code, 'failing test was not filtered out'); 20 | }); 21 | setTimeout(function() { 22 | proc.kill('SIGINT'); 23 | }, 1000); 24 | }, 25 | }; -------------------------------------------------------------------------------- /test/local-assert.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // Using a localized version of assert. 4 | exports['first'] = function(beforeExit, assert) { 5 | process.nextTick(function() { 6 | assert.equal(assert._test.suite, 'local-assert.test.js'); 7 | assert.equal(assert._test.title, 'first'); 8 | try { 9 | var error = false; 10 | assert.ok(false); 11 | } catch (err) { 12 | assert.equal(err._test, assert._test); 13 | error = true; 14 | } 15 | assert.ok(error); 16 | }); 17 | }; 18 | 19 | // Using the broken global version of assert. 20 | exports['second'] = function(beforeExit) { 21 | process.nextTick(function() { 22 | assert.notEqual(assert._test.suite, 'local-assert.test.js'); 23 | assert.notEqual(assert._test.title, 'third'); 24 | }); 25 | }; 26 | 27 | // Overwrite the testTitle in assert. 28 | exports['third'] = function() {}; 29 | -------------------------------------------------------------------------------- /docs/layout/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Expresso - TDD Framework For Node 4 | 36 | 37 | 38 | 39 | Fork me on GitHub 40 | 41 |
42 |

Expresso 0.9.0

43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PREFIX ?= /usr/local 3 | BIN = bin/expresso 4 | JSCOV = deps/jscoverage/node-jscoverage 5 | DOCS = docs/index.md 6 | HTMLDOCS = $(DOCS:.md=.html) 7 | 8 | test: $(BIN) 9 | @./$(BIN) --growl $(TEST_FLAGS) 10 | 11 | test-cov: 12 | @./$(BIN) -I lib --cov $(TEST_FLAGS) 13 | 14 | test-serial: 15 | @./$(BIN) --serial $(TEST_FLAGS) test/serial/*.test.js 16 | 17 | install: install-jscov install-expresso 18 | 19 | uninstall: 20 | rm -f $(PREFIX)/bin/expresso 21 | rm -f $(PREFIX)/bin/node-jscoverage 22 | 23 | install-jscov: $(JSCOV) 24 | install $(JSCOV) $(PREFIX)/bin 25 | 26 | install-expresso: 27 | install $(BIN) $(PREFIX)/bin 28 | 29 | $(JSCOV): 30 | cd deps/jscoverage && ./configure && make && mv jscoverage node-jscoverage 31 | 32 | clean: 33 | @cd deps/jscoverage && git clean -fd 34 | 35 | docs: docs/api.html $(HTMLDOCS) 36 | 37 | %.html: %.md 38 | @echo "... $< > $@" 39 | @ronn --html $< \ 40 | | cat docs/layout/head.html - docs/layout/foot.html \ 41 | > $@ 42 | 43 | docs/api.html: bin/expresso 44 | dox \ 45 | --title "Expresso" \ 46 | --ribbon "http://github.com/visionmedia/expresso" \ 47 | --desc "Insanely fast TDD framework for [node](http://nodejs.org) featuring code coverage reporting." \ 48 | $< > $@ 49 | 50 | docclean: 51 | rm -f docs/*.html 52 | 53 | .PHONY: test test-cov install uninstall install-expresso install-jscov clean docs docclean -------------------------------------------------------------------------------- /test/serial/http.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | if (req.method === 'GET') { 11 | if (req.url === '/delay') { 12 | setTimeout(function(){ 13 | res.writeHead(200, {}); 14 | res.end('delayed'); 15 | }, 200); 16 | } else { 17 | var body = JSON.stringify({ name: 'tj' }); 18 | res.writeHead(200, { 19 | 'Content-Type': 'application/json; charset=utf8', 20 | 'Content-Length': body.length 21 | }); 22 | res.end(body); 23 | } 24 | } else { 25 | var body = ''; 26 | req.setEncoding('utf8'); 27 | req.addListener('data', function(chunk){ body += chunk }); 28 | req.addListener('end', function(){ 29 | res.writeHead(200, {}); 30 | res.end(req.url + ' ' + body); 31 | }); 32 | } 33 | }); 34 | 35 | module.exports = { 36 | 'test assert.response()': function(done){ 37 | assert.response(server, { 38 | url: '/', 39 | method: 'GET' 40 | },{ 41 | body: '{"name":"tj"}', 42 | status: 200, 43 | headers: { 44 | 'Content-Type': 'application/json; charset=utf8' 45 | } 46 | }, done); 47 | } 48 | }; -------------------------------------------------------------------------------- /test/assert.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | 8 | module.exports = { 9 | 'assert.eql()': function(){ 10 | assert.equal(assert.deepEqual, assert.eql); 11 | }, 12 | 13 | 'assert.type()': function(){ 14 | assert.type('foobar', 'string'); 15 | assert.type(2, 'number'); 16 | assert.throws(function(){ 17 | assert.type([1,2,3], 'string'); 18 | }); 19 | }, 20 | 21 | 'assert.includes()': function(){ 22 | assert.includes('some random string', 'dom'); 23 | assert.throws(function(){ 24 | assert.include('some random string', 'foobar'); 25 | }); 26 | 27 | assert.includes(['foo', 'bar'], 'bar'); 28 | assert.includes(['foo', 'bar'], 'foo'); 29 | assert.includes([1,2,3], 3); 30 | assert.includes([1,2,3], 2); 31 | assert.includes([1,2,3], 1); 32 | assert.throws(function(){ 33 | assert.includes(['foo', 'bar'], 'baz'); 34 | }); 35 | 36 | assert.throws(function(){ 37 | assert.includes({ wrong: 'type' }, 'foo'); 38 | }); 39 | }, 40 | 41 | 'assert.isNull()': function(){ 42 | assert.isNull(null); 43 | assert.throws(function(){ 44 | assert.isNull(undefined); 45 | }); 46 | assert.throws(function(){ 47 | assert.isNull(false); 48 | }); 49 | }, 50 | 51 | 'assert.isUndefined()': function(){ 52 | assert.isUndefined(undefined); 53 | assert.throws(function(){ 54 | assert.isUndefined(null); 55 | }); 56 | assert.throws(function(){ 57 | assert.isUndefined(false); 58 | }); 59 | }, 60 | 61 | 'assert.isNotNull()': function(){ 62 | assert.isNotNull(false); 63 | assert.isNotNull(undefined); 64 | assert.throws(function(){ 65 | assert.isNotNull(null); 66 | }); 67 | }, 68 | 69 | 'assert.isDefined()': function(){ 70 | assert.isDefined(false); 71 | assert.isDefined(null); 72 | assert.throws(function(){ 73 | assert.isDefined(undefined); 74 | }); 75 | }, 76 | 77 | 'assert.match()': function(){ 78 | assert.match('foobar', /foo(bar)?/); 79 | assert.throws(function(){ 80 | assert.match('something', /rawr/); 81 | }); 82 | } 83 | }; -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Expresso 2 | 3 | TDD framework for [nodejs](http://nodejs.org). This module is 4 | no longer maintained by me, if you wish to maintain it let me know, 5 | otherwise use [Mocha](https://github.com/mochajs/mocha). 6 | 7 | ## Features 8 | 9 | - light-weight 10 | - intuitive async support 11 | - intuitive test runner executable 12 | - test coverage support and reporting 13 | - uses the _assert_ module 14 | - `assert.eql()` alias of `assert.deepEqual()` 15 | - `assert.response()` http response utility 16 | - `assert.includes()` 17 | - `assert.type()` 18 | - `assert.isNull()` 19 | - `assert.isUndefined()` 20 | - `assert.isNotNull()` 21 | - `assert.isDefined()` 22 | - `assert.match()` 23 | - `assert.length()` 24 | 25 | ## Installation 26 | 27 | To install both expresso _and_ node-jscoverage run: 28 | 29 | $ make install 30 | 31 | To install expresso alone (no build required) run: 32 | 33 | $ make install-expresso 34 | 35 | Install via npm: 36 | 37 | $ npm install expresso 38 | 39 | ## Usage 40 | 41 | Create a directory named `test` in your project and place JavaScript files in it. Each JavaScript file can export multiple tests of this format: 42 | 43 | ```js 44 | exports['test String#length'] = function(beforeExit, assert) { 45 | assert.equal(6, 'foobar'.length); 46 | }; 47 | ``` 48 | 49 | To run tests, type `expresso`. 50 | 51 | For more information, [see the manual](http://visionmedia.github.com/expresso/). 52 | 53 | ## License 54 | 55 | (The MIT License) 56 | 57 | Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca> 58 | 59 | Permission is hereby granted, free of charge, to any person obtaining 60 | a copy of this software and associated documentation files (the 61 | 'Software'), to deal in the Software without restriction, including 62 | without limitation the rights to use, copy, modify, merge, publish, 63 | distribute, sublicense, and/or sell copies of the Software, and to 64 | permit persons to whom the Software is furnished to do so, subject to 65 | the following conditions: 66 | 67 | The above copyright notice and this permission notice shall be 68 | included in all copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 71 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 72 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 73 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 74 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 75 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 76 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 77 | -------------------------------------------------------------------------------- /test/http.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | if (req.method === 'GET') { 11 | if (req.url === '/delay') { 12 | setTimeout(function(){ 13 | res.writeHead(200, {}); 14 | res.end('delayed'); 15 | }, 200); 16 | } else { 17 | var body = JSON.stringify({ name: 'tj' }); 18 | res.writeHead(200, { 19 | 'Content-Type': 'application/json; charset=utf8', 20 | 'Content-Length': body.length 21 | }); 22 | res.end(body); 23 | } 24 | } else { 25 | var body = ''; 26 | req.setEncoding('utf8'); 27 | req.on('data', function(chunk){ body += chunk }); 28 | req.on('end', function(){ 29 | res.writeHead(200, {}); 30 | res.end(req.url + ' ' + body); 31 | }); 32 | } 33 | }); 34 | 35 | var delayedServer = http.createServer(function(req, res){ 36 | res.writeHead(200); 37 | res.end('it worked'); 38 | }); 39 | 40 | var oldListen = delayedServer.listen; 41 | delayedServer.listen = function(){ 42 | var args = arguments; 43 | setTimeout(function(){ 44 | oldListen.apply(delayedServer, args); 45 | }, 100); 46 | }; 47 | 48 | var runningServer = http.createServer(function(req, res){ 49 | res.writeHead(200); 50 | res.end('it worked'); 51 | }); 52 | 53 | runningServer.listen(5554); 54 | 55 | module.exports = { 56 | 'test assert.response(req, res, fn)': function(beforeExit){ 57 | var calls = 0; 58 | 59 | assert.response(server, { 60 | url: '/', 61 | method: 'GET' 62 | },{ 63 | body: '{"name":"tj"}', 64 | status: 200, 65 | headers: { 66 | 'Content-Type': 'application/json; charset=utf8' 67 | } 68 | }, function(res){ 69 | ++calls; 70 | assert.ok(res); 71 | }); 72 | 73 | beforeExit(function(){ 74 | assert.equal(1, calls); 75 | }) 76 | }, 77 | 78 | 'test assert.response(req, fn)': function(beforeExit){ 79 | var calls = 0; 80 | 81 | assert.response(server, { 82 | url: '/foo' 83 | }, function(res){ 84 | ++calls; 85 | assert.ok(res.body.indexOf('tj') >= 0, 'Test assert.response() callback'); 86 | }); 87 | 88 | beforeExit(function(){ 89 | assert.equal(1, calls); 90 | }); 91 | }, 92 | 93 | 'test assert.response() delay': function(beforeExit){ 94 | var calls = 0; 95 | 96 | assert.response(server, 97 | { url: '/delay', timeout: 1500 }, 98 | { body: 'delayed' }, 99 | function(){ 100 | ++calls; 101 | }); 102 | 103 | beforeExit(function(){ 104 | assert.equal(1, calls); 105 | }); 106 | }, 107 | 108 | 'test assert.response() regexp': function(beforeExit){ 109 | var calls = 0; 110 | 111 | assert.response(server, 112 | { url: '/foo', method: 'POST', data: 'foobar' }, 113 | { body: /^\/foo foo(bar)?/ }, 114 | function(){ 115 | ++calls; 116 | }); 117 | 118 | beforeExit(function(){ 119 | assert.equal(1, calls); 120 | }); 121 | }, 122 | 123 | 'test assert.response() regexp headers': function(beforeExit){ 124 | var calls = 0; 125 | 126 | assert.response(server, 127 | { url: '/' }, 128 | { body: '{"name":"tj"}', headers: { 'Content-Type': /^application\/json/ } }, 129 | function(){ 130 | ++calls; 131 | }); 132 | 133 | beforeExit(function(){ 134 | assert.equal(1, calls); 135 | }); 136 | }, 137 | 138 | // [!] if this test doesn't pass, an uncaught ECONNREFUSED will display 139 | 'test assert.response() with deferred listen()': function(beforeExit){ 140 | var calls = 0; 141 | 142 | assert.response(delayedServer, 143 | { url: '/' }, 144 | { body: 'it worked' }, 145 | function(){ 146 | ++calls; 147 | }); 148 | 149 | beforeExit(function(){ 150 | assert.equal(1, calls); 151 | }); 152 | }, 153 | 154 | 'test assert.response() with already running server': function(beforeExit){ 155 | var calls = 0; 156 | 157 | assert.response(runningServer, 158 | { url: '/' }, 159 | { body: 'it worked' }, 160 | function(){ 161 | ++calls; 162 | }); 163 | 164 | beforeExit(function(){ 165 | assert.equal(1, calls); 166 | }); 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.9.2 / 2011-10-12 3 | ================== 4 | 5 | * Fixed serial support 6 | 7 | 0.9.1 / 2011-10-12 8 | ================== 9 | 10 | * Added 0.5.x support 11 | * Added regex based test name filtering argument 12 | * Removed "sys" module related calls 13 | 14 | 0.8.1 / 2011-07-07 15 | ================== 16 | 17 | * Revert "Changed: explicitly exit on failure" 18 | * acquire port number from servers that are already running and wait until server started up 19 | 20 | 0.8.0 / 2011-07-05 21 | ================== 22 | 23 | * Changed: explicitly exit on failure 24 | 25 | 0.7.9 / 2011-06-22 26 | ================== 27 | 28 | * Fixed; increase maxListeners to prevent reaching the limit 29 | 30 | 0.7.8 / 2011-06-22 31 | ================== 32 | 33 | * Added `--json [file]` flag to output JSON test coverage reports 34 | 35 | 0.7.7 / 2011-05-24 36 | ================== 37 | 38 | * Moved to new http request API. Fixing Connection Refused error [Renault John Lecoultre] 39 | * Added encoding option to request function. Defaults to utf8 as before. [nateps] 40 | 41 | 0.7.6 / 2011-04-20 42 | ================== 43 | 44 | * Added teardown for serial tests. Sync tests no longer require parameter. [Raevel] 45 | 46 | 0.7.5 / 2011-03-31 47 | ================== 48 | 49 | * Fixed; removed some ansi-escape sequences in --boring mode [Norbert Schultz] 50 | 51 | 0.7.4 / 2011-03-27 52 | ================== 53 | 54 | * Added coffee-script support (wahoo...) 55 | 56 | 0.7.3 / 2011-03-02 57 | ================== 58 | 59 | * Fixed server listening check. Closes #62. [Andrew Stone] 60 | 61 | 0.7.2 / 2010-12-29 62 | ================== 63 | 64 | * Fixed problem with `listen()` sometimes firing on the same tick [guillermo] 65 | 66 | 0.7.1 / 2010-12-28 67 | ================== 68 | 69 | * Fixed `assert.request()` client logic into an issue() function, fired upon the `listen()` callback if the server doesn't have an assigned fd. [guillermo] 70 | * Removed `--watch` 71 | 72 | 0.7.0 / 2010-11-19 73 | ================== 74 | 75 | * Removed `assert` from test function signature 76 | Just use `require('assert')` :) this will make integration 77 | with libraries like [should](http://github.com/visionmedia/should) cleaner. 78 | 79 | 0.6.4 / 2010-11-02 80 | ================== 81 | 82 | * Added regexp support to `assert.response()` headers 83 | * Removed `waitForExit` code, causing issues 84 | 85 | 0.6.3 / 2010-11-02 86 | ================== 87 | 88 | * Added `assert.response()` body RegExp support 89 | * Fixed issue with _--serial_ not executing files sequentially. Closes #42 90 | * Fixed hang when modules use `setInterval` - monitor running tests & force the process to quit after all have completed + timeout [Steve Mason] 91 | 92 | 0.6.2 / 2010-09-17 93 | ================== 94 | 95 | * Added _node-jsocoverage_ to package.json (aka will respect npm's binroot) 96 | * Added _-t, --timeout_ MS option, defaulting to 2000 ms 97 | * Added _-s, --serial_ 98 | * __PREFIX__ clobberable 99 | * Fixed `assert.response()` for latest node 100 | * Fixed cov reporting from exploding on empty files 101 | 102 | 0.6.2 / 2010-08-03 103 | ================== 104 | 105 | * Added `assert.type()` 106 | * Renamed `assert.isNotUndefined()` to `assert.isDefined()` 107 | * Fixed `assert.includes()` param ordering 108 | 109 | 0.6.0 / 2010-07-31 110 | ================== 111 | 112 | * Added _docs/api.html_ 113 | * Added -w, --watch 114 | * Added `Array` support to `assert.includes()` 115 | * Added; outputting exceptions immediately. Closes #19 116 | * Fixed `assert.includes()` param ordering 117 | * Fixed `assert.length()` param ordering 118 | * Fixed jscoverage links 119 | 120 | 0.5.0 / 2010-07-16 121 | ================== 122 | 123 | * Added support for async exports 124 | * Added timeout support to `assert.response()`. Closes #3 125 | * Added 4th arg callback support to `assert.response()` 126 | * Added `assert.length()` 127 | * Added `assert.match()` 128 | * Added `assert.isUndefined()` 129 | * Added `assert.isNull()` 130 | * Added `assert.includes()` 131 | * Added growlnotify support via -g, --growl 132 | * Added -o, --only TESTS. Ex: --only "test foo()" --only "test foo(), test bar()" 133 | * Removed profanity 134 | 135 | 0.4.0 / 2010-07-09 136 | ================== 137 | 138 | * Added reporting source coverage (respects --boring for color haters) 139 | * Added callback to assert.response(). Closes #12 140 | * Fixed; putting exceptions to stderr. Closes #13 141 | 142 | 0.3.1 / 2010-06-28 143 | ================== 144 | 145 | * Faster assert.response() 146 | 147 | 0.3.0 / 2010-06-28 148 | ================== 149 | 150 | * Added -p, --port NUM flags 151 | * Added assert.response(). Closes #11 152 | 153 | 0.2.1 / 2010-06-25 154 | ================== 155 | 156 | * Fixed issue with reporting object assertions 157 | 158 | 0.2.0 / 2010-06-21 159 | ================== 160 | 161 | * Added `make uninstall` 162 | * Added better readdir() failure message 163 | * Fixed `make install` for kiwi 164 | 165 | 0.1.0 / 2010-06-15 166 | ================== 167 | 168 | * Added better usage docs via --help 169 | * Added better conditional color support 170 | * Added pre exit assertion support 171 | 172 | 0.0.3 / 2010-06-02 173 | ================== 174 | 175 | * Added more room for filenames in test coverage 176 | * Added boring output support via --boring (suppress colored output) 177 | * Fixed async failure exit status 178 | 179 | 0.0.2 / 2010-05-30 180 | ================== 181 | 182 | * Fixed exit status for CI support 183 | 184 | 0.0.1 / 2010-05-30 185 | ================== 186 | 187 | * Initial release -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | [Expresso](http://github.com/visionmedia/expresso) is a JavaScript [TDD](http://en.wikipedia.org/wiki/Test-driven_development) framework written for [nodejs](http://nodejs.org). Expresso is extremely fast, and is packed with features such as additional assertion methods, code coverage reporting, CI support, and more. 3 | 4 | ## Features 5 | 6 | - light-weight 7 | - intuitive async support 8 | - intuitive test runner executable 9 | - test coverage support and reporting via [node-jscoverage](http://github.com/visionmedia/node-jscoverage) 10 | - uses and extends the core `assert` module 11 | - `assert.eql()` alias of `assert.deepEqual()` 12 | - `assert.response()` http response utility 13 | - `assert.includes()` 14 | - `assert.isNull()` 15 | - `assert.isUndefined()` 16 | - `assert.isNotNull()` 17 | - `assert.isDefined()` 18 | - `assert.match()` 19 | - `assert.length()` 20 | 21 | ## Installation 22 | 23 | To install both expresso _and_ node-jscoverage run 24 | the command below, which will first compile node-jscoverage: 25 | 26 | $ make install 27 | 28 | To install expresso alone without coverage reporting run: 29 | 30 | $ make install-expresso 31 | 32 | Install via npm: 33 | 34 | $ npm install expresso 35 | 36 | ## Examples 37 | 38 | To define tests we simply export several functions: 39 | 40 | exports['test String#length'] = function(){ 41 | assert.equal(6, 'foobar'.length); 42 | }; 43 | 44 | Alternatively for large numbers of tests you may want to 45 | export your own object containing the tests, however this 46 | is essentially the as above: 47 | 48 | module.exports = { 49 | 'test String#length': function(beforeExit, assert) { 50 | assert.equal(6, 'foobar'.length); 51 | } 52 | }; 53 | 54 | If you prefer not to use quoted keys: 55 | 56 | exports.testsStringLength = function(beforeExit, assert) { 57 | assert.equal(6, 'foobar'.length); 58 | }; 59 | 60 | The argument passed to each callback is `beforeExit` and `assert`. 61 | The context ("`this`") of each test function is a _Test_ object. You can pass a function to `beforeExit` to make sure the assertions are run before the tests exit. This is can be used to verify that tests have indeed been run. `beforeExit` is a shortcut for listening to the `exit` event on `this`. The second parameter `assert` is the `assert` object localized to that test. It makes sure that assertions in asynchronous callbacks are associated with the correct test. 62 | 63 | exports.testAsync = function(beforeExit, assert) { 64 | var n = 0; 65 | setTimeout(function() { 66 | ++n; 67 | assert.ok(true); 68 | }, 200); 69 | setTimeout(function() { 70 | ++n; 71 | assert.ok(true); 72 | }, 200); 73 | 74 | // When the tests are finished, the exit event is emitted. 75 | this.on('exit', function() { 76 | assert.equal(2, n, 'Ensure both timeouts are called'); 77 | }); 78 | 79 | // Alternatively, you can use the beforeExit shortcut. 80 | beforeExit(function() { 81 | assert.equal(2, n, 'Ensure both timeouts are called'); 82 | }); 83 | }; 84 | 85 | ## Assert Utilities 86 | 87 | ### assert.isNull(val[, msg]) 88 | 89 | Asserts that the given `val` is `null`. 90 | 91 | assert.isNull(null); 92 | 93 | ### assert.isNotNull(val[, msg]) 94 | 95 | Asserts that the given `val` is not `null`. 96 | 97 | assert.isNotNull(undefined); 98 | assert.isNotNull(false); 99 | 100 | ### assert.isUndefined(val[, msg]) 101 | 102 | Asserts that the given `val` is `undefined`. 103 | 104 | assert.isUndefined(undefined); 105 | 106 | ### assert.isDefined(val[, msg]) 107 | 108 | Asserts that the given `val` is not `undefined`. 109 | 110 | assert.isDefined(null); 111 | assert.isDefined(false); 112 | 113 | ### assert.match(str, regexp[, msg]) 114 | 115 | Asserts that the given `str` matches `regexp`. 116 | 117 | assert.match('foobar', /^foo(bar)?/); 118 | assert.match('foo', /^foo(bar)?/); 119 | 120 | ### assert.length(val, n[, msg]) 121 | 122 | Assert that the given `val` has a length of `n`. 123 | 124 | assert.length([1,2,3], 3); 125 | assert.length('foo', 3); 126 | 127 | ### assert.type(obj, type[, msg]) 128 | 129 | Assert that the given `obj` is typeof `type`. 130 | 131 | assert.type(3, 'number'); 132 | 133 | ### assert.eql(a, b[, msg]) 134 | 135 | Assert that object `b` is equal to object `a`. This is an 136 | alias for the core `assert.deepEqual()` method which does complex 137 | comparisons, opposed to `assert.equal()` which uses `==`. 138 | 139 | assert.eql('foo', 'foo'); 140 | assert.eql([1,2], [1,2]); 141 | assert.eql({ foo: 'bar' }, { foo: 'bar' }); 142 | 143 | ### assert.includes(obj, val[, msg]) 144 | 145 | Assert that `obj` is within `val`. This method supports `Array`s 146 | and `Strings`s. 147 | 148 | assert.includes([1,2,3], 3); 149 | assert.includes('foobar', 'foo'); 150 | assert.includes('foobar', 'bar'); 151 | 152 | ### assert.response(server, req, res|fn[, msg|fn]) 153 | 154 | Performs assertions on the given `server`, which should _not_ call 155 | `listen()`, as this is handled internally by expresso and the server 156 | is killed after all responses have completed. This method works with 157 | any `http.Server` instance, so _Connect_ and _Express_ servers will work 158 | as well. 159 | 160 | The **`req`** object may contain: 161 | 162 | - `url`: request url 163 | - `timeout`: timeout in milliseconds 164 | - `method`: HTTP method 165 | - `data`: request body 166 | - `headers`: headers object 167 | 168 | The **`res`** object may be a callback function which 169 | receives the response for assertions, or an object 170 | which is then used to perform several assertions 171 | on the response with the following properties: 172 | 173 | - `body`: assert response body (regexp or string) 174 | - `status`: assert response status code 175 | - `header`: assert that all given headers match (unspecified are ignored, use a regexp or string) 176 | 177 | When providing `res` you may then also pass a callback function 178 | as the fourth argument for additional assertions. 179 | 180 | Below are some examples: 181 | 182 | assert.response(server, { 183 | url: '/', timeout: 500 184 | }, { 185 | body: 'foobar' 186 | }); 187 | 188 | assert.response(server, { 189 | url: '/', 190 | method: 'GET' 191 | }, { 192 | body: '{"name":"tj"}', 193 | status: 200, 194 | headers: { 195 | 'Content-Type': 'application/json; charset=utf8', 196 | 'X-Foo': 'bar' 197 | } 198 | }); 199 | 200 | assert.response(server, { 201 | url: '/foo', 202 | method: 'POST', 203 | data: 'bar baz' 204 | }, { 205 | body: '/foo bar baz', 206 | status: 200 207 | }, 'Test POST'); 208 | 209 | assert.response(server, { 210 | url: '/foo', 211 | method: 'POST', 212 | data: 'bar baz' 213 | }, { 214 | body: '/foo bar baz', 215 | status: 200 216 | }, function(res){ 217 | // All done, do some more tests if needed 218 | }); 219 | 220 | assert.response(server, { 221 | url: '/' 222 | }, function(res){ 223 | assert.ok(res.body.indexOf('tj') >= 0, 'Test assert.response() callback'); 224 | }); 225 | 226 | This function will fail when it receives no response or when the timeout (default is 30 seconds) expires. 227 | 228 | 229 | ## expresso(1) 230 | 231 | To run a single test suite (file) run: 232 | 233 | $ expresso test/a.test.js 234 | 235 | To run several suites we may simply append another: 236 | 237 | $ expresso test/a.test.js test/b.test.js 238 | 239 | We can also pass a whitelist of tests to run within all suites: 240 | 241 | $ expresso --only "foo()" --only "bar()" 242 | 243 | Or several with one call: 244 | 245 | $ expresso --only "foo(), bar()" 246 | 247 | Globbing is of course possible as well: 248 | 249 | $ expresso test/* 250 | 251 | When expresso is called without any files, _test/*_ is the default, 252 | so the following is equivalent to the command above: 253 | 254 | $ expresso 255 | 256 | If you wish to unshift a path to `require.paths` before 257 | running tests, you may use the `-I` or `--include` flag. 258 | 259 | $ expresso --include lib test/* 260 | 261 | The previous example is typically what I would recommend, since expresso 262 | supports test coverage via [node-jscoverage](http://github.com/visionmedia/node-jscoverage) (bundled with expresso), 263 | so you will need to expose an instrumented version of you library. 264 | 265 | To instrument your library, simply run [node-jscoverage](http://github.com/visionmedia/node-jscoverage), 266 | passing the _src_ and _dest_ directories: 267 | 268 | $ node-jscoverage lib lib-cov 269 | 270 | Now we can run our tests again, using the _lib-cov_ directory that has been 271 | instrumented with coverage statements: 272 | 273 | $ expresso -I lib-cov test/* 274 | 275 | The output will look similar to below, depending on your test coverage of course :) 276 | 277 | ![node coverage](http://dl.dropbox.com/u/6396913/cov.png) 278 | 279 | To make this process easier expresso has the `-c` or `--cov` which essentially 280 | does the same as the two commands above. The following two commands will 281 | run the same tests, however one will auto-instrument, and unshift `lib-cov`, 282 | and the other will run tests normally: 283 | 284 | $ expresso -I lib test/* 285 | $ expresso -I lib --cov test/* 286 | 287 | Currently coverage is bound to the `lib` directory, however in the 288 | future `--cov` will most likely accept a path. 289 | 290 | If you would like code coverage reports suitable for automated parsing, pass the `--json [output file]` option: 291 | 292 | $ expresso -I lib test/* 293 | $ expresso -I lib --cov --json coverage.json test/* 294 | 295 | You should then see the json coverage details in the file you specified: 296 | 297 | { 298 | "LOC": 20, 299 | "SLOC": 7, 300 | "coverage": "71.43", 301 | "files": { 302 | "bar.js": { 303 | "LOC": 4, 304 | "SLOC": 2, 305 | "coverage": "100.00", 306 | "totalMisses": 0 307 | }, 308 | "foo.js": { 309 | "LOC": 16, 310 | "SLOC": 5, 311 | "coverage": "60.00", 312 | "totalMisses": 2 313 | } 314 | }, 315 | "totalMisses": 2 316 | } 317 | 318 | ## Async Exports 319 | 320 | Sometimes it is useful to postpone running of tests until a callback or event has fired, currently the `exports.foo = function() {};` syntax is supported for this: 321 | 322 | setTimeout(function() { 323 | exports['test async exports'] = function(){ 324 | assert.ok('wahoo'); 325 | }; 326 | }, 100); 327 | 328 | Note that you only have one "shot" at exporting. You have to export all of your test functions in the same loop as the first one. That means you can't progressively add more test functions to the `exports` object. 329 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Expresso - TDD Framework For Node 4 | 36 | 37 | 38 | 39 | Fork me on GitHub 40 | 41 |
42 |

Expresso 0.9.0

43 |

Expresso is a JavaScript TDD framework written for nodejs. Expresso is extremely fast, and is packed with features such as additional assertion methods, code coverage reporting, CI support, and more.

Features

  • light-weight
  • intuitive async support
  • intuitive test runner executable
  • test coverage support and reporting via node-jscoverage
  • uses and extends the core assert module
  • assert.eql() alias of assert.deepEqual()
  • assert.response() http response utility
  • assert.includes()
  • assert.isNull()
  • assert.isUndefined()
  • assert.isNotNull()
  • assert.isDefined()
  • assert.match()
  • assert.length()

Installation

To install both expresso and node-jscoverage run 44 | the command below, which will first compile node-jscoverage:

$ make install

To install expresso alone without coverage reporting run:

$ make install-expresso

Install via npm:

$ npm install expresso

Examples

To define tests we simply export several functions:

exports['test String#length'] = function(){
 45 |     assert.equal(6, 'foobar'.length);
 46 | };

Alternatively for large numbers of tests you may want to 47 | export your own object containing the tests, however this 48 | is essentially the as above:

module.exports = {
 49 |     'test String#length': function(beforeExit, assert) {
 50 |       assert.equal(6, 'foobar'.length);
 51 |     }
 52 | };

If you prefer not to use quoted keys:

exports.testsStringLength = function(beforeExit, assert) {
 53 |     assert.equal(6, 'foobar'.length);
 54 | };

The argument passed to each callback is beforeExit and assert. 55 | The context ("this") of each test function is a Test object. You can pass a function to beforeExit to make sure the assertions are run before the tests exit. This is can be used to verify that tests have indeed been run. beforeExit is a shortcut for listening to the exit event on this. The second parameter assert is the assert object localized to that test. It makes sure that assertions in asynchronous callbacks are associated with the correct test.

exports.testAsync = function(beforeExit, assert) {
 56 |     var n = 0;
 57 |     setTimeout(function() {
 58 |         ++n;
 59 |         assert.ok(true);
 60 |     }, 200);
 61 |     setTimeout(function() {
 62 |         ++n;
 63 |         assert.ok(true);
 64 |     }, 200);
 65 | 
 66 |     // When the tests are finished, the exit event is emitted.
 67 |     this.on('exit', function() {
 68 |         assert.equal(2, n, 'Ensure both timeouts are called');
 69 |     });
 70 | 
 71 |     // Alternatively, you can use the beforeExit shortcut.
 72 |     beforeExit(function() {
 73 |         assert.equal(2, n, 'Ensure both timeouts are called');
 74 |     });
 75 | };

Assert Utilities

assert.isNull(val[, msg])

Asserts that the given val is null.

assert.isNull(null);

assert.isNotNull(val[, msg])

Asserts that the given val is not null.

assert.isNotNull(undefined);
 76 | assert.isNotNull(false);

assert.isUndefined(val[, msg])

Asserts that the given val is undefined.

assert.isUndefined(undefined);

assert.isDefined(val[, msg])

Asserts that the given val is not undefined.

assert.isDefined(null);
 77 | assert.isDefined(false);

assert.match(str, regexp[, msg])

Asserts that the given str matches regexp.

assert.match('foobar', /^foo(bar)?/);
 78 | assert.match('foo', /^foo(bar)?/);

assert.length(val, n[, msg])

Assert that the given val has a length of n.

assert.length([1,2,3], 3);
 79 | assert.length('foo', 3);

assert.type(obj, type[, msg])

Assert that the given obj is typeof type.

assert.type(3, 'number');

assert.eql(a, b[, msg])

Assert that object b is equal to object a. This is an 80 | alias for the core assert.deepEqual() method which does complex 81 | comparisons, opposed to assert.equal() which uses ==.

assert.eql('foo', 'foo');
 82 | assert.eql([1,2], [1,2]);
 83 | assert.eql({ foo: 'bar' }, { foo: 'bar' });

assert.includes(obj, val[, msg])

Assert that obj is within val. This method supports Arrays 84 | and Stringss.

assert.includes([1,2,3], 3);
 85 | assert.includes('foobar', 'foo');
 86 | assert.includes('foobar', 'bar');

assert.response(server, req, res|fn[, msg|fn])

Performs assertions on the given server, which should not call 87 | listen(), as this is handled internally by expresso and the server 88 | is killed after all responses have completed. This method works with 89 | any http.Server instance, so Connect and Express servers will work 90 | as well.

The req object may contain:

  • url: request url
  • timeout: timeout in milliseconds
  • method: HTTP method
  • data: request body
  • headers: headers object

The res object may be a callback function which 91 | receives the response for assertions, or an object 92 | which is then used to perform several assertions 93 | on the response with the following properties:

  • body: assert response body (regexp or string)
  • status: assert response status code
  • header: assert that all given headers match (unspecified are ignored, use a regexp or string)

When providing res you may then also pass a callback function 94 | as the fourth argument for additional assertions.

Below are some examples:

assert.response(server, {
 95 |     url: '/', timeout: 500
 96 | }, {
 97 |     body: 'foobar'
 98 | });
 99 | 
100 | assert.response(server, {
101 |     url: '/',
102 |     method: 'GET'
103 | }, {
104 |     body: '{"name":"tj"}',
105 |     status: 200,
106 |     headers: {
107 |         'Content-Type': 'application/json; charset=utf8',
108 |         'X-Foo': 'bar'
109 |     }
110 | });
111 | 
112 | assert.response(server, {
113 |     url: '/foo',
114 |     method: 'POST',
115 |     data: 'bar baz'
116 | }, {
117 |     body: '/foo bar baz',
118 |     status: 200
119 | }, 'Test POST');
120 | 
121 | assert.response(server, {
122 |     url: '/foo',
123 |     method: 'POST',
124 |     data: 'bar baz'
125 | }, {
126 |     body: '/foo bar baz',
127 |     status: 200
128 | }, function(res){
129 |     // All done, do some more tests if needed
130 | });
131 | 
132 | assert.response(server, {
133 |     url: '/'
134 | }, function(res){
135 |     assert.ok(res.body.indexOf('tj') >= 0, 'Test assert.response() callback');
136 | });

This function will fail when it receives no response or when the timeout (default is 30 seconds) expires.

expresso(1)

To run a single test suite (file) run:

$ expresso test/a.test.js

To run several suites we may simply append another:

$ expresso test/a.test.js test/b.test.js

We can also pass a whitelist of tests to run within all suites:

$ expresso --only "foo()" --only "bar()"

Or several with one call:

$ expresso --only "foo(), bar()"

Globbing is of course possible as well:

$ expresso test/*

When expresso is called without any files, test/* is the default, 137 | so the following is equivalent to the command above:

$ expresso

If you wish to unshift a path to require.paths before 138 | running tests, you may use the -I or --include flag.

$ expresso --include lib test/*

The previous example is typically what I would recommend, since expresso 139 | supports test coverage via node-jscoverage (bundled with expresso), 140 | so you will need to expose an instrumented version of you library.

To instrument your library, simply run node-jscoverage, 141 | passing the src and dest directories:

$ node-jscoverage lib lib-cov

Now we can run our tests again, using the lib-cov directory that has been 142 | instrumented with coverage statements:

$ expresso -I lib-cov test/*

The output will look similar to below, depending on your test coverage of course :)

node coverage

To make this process easier expresso has the -c or --cov which essentially 143 | does the same as the two commands above. The following two commands will 144 | run the same tests, however one will auto-instrument, and unshift lib-cov, 145 | and the other will run tests normally:

$ expresso -I lib test/*
146 | $ expresso -I lib --cov test/*

Currently coverage is bound to the lib directory, however in the 147 | future --cov will most likely accept a path.

If you would like code coverage reports suitable for automated parsing, pass the --json [output file] option:

$ expresso -I lib test/*
148 | $ expresso -I lib --cov --json coverage.json test/*

You should then see the json coverage details in the file you specified:

{
149 |     "LOC": 20,
150 |     "SLOC": 7,
151 |     "coverage": "71.43",
152 |     "files": {
153 |         "bar.js": {
154 |             "LOC": 4,
155 |             "SLOC": 2,
156 |             "coverage": "100.00",
157 |             "totalMisses": 0
158 |         },
159 |         "foo.js": {
160 |             "LOC": 16,
161 |             "SLOC": 5,
162 |             "coverage": "60.00",
163 |             "totalMisses": 2
164 |         }
165 |     },
166 |     "totalMisses": 2
167 | }

Async Exports

Sometimes it is useful to postpone running of tests until a callback or event has fired, currently the exports.foo = function() {}; syntax is supported for this:

setTimeout(function() {
168 |     exports['test async exports'] = function(){
169 |         assert.ok('wahoo');
170 |     };
171 | }, 100);

Note that you only have one "shot" at exporting. You have to export all of your test functions in the same loop as the first one. That means you can't progressively add more test functions to the exports object.

172 |
173 | 174 | -------------------------------------------------------------------------------- /bin/expresso: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Expresso 5 | * Copyright(c) TJ Holowaychuk 6 | * (MIT Licensed) 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var assert = require('assert'), 14 | childProcess = require('child_process'), 15 | http = require('http'), 16 | path = require('path'), 17 | util = require('util'), 18 | cwd = process.cwd(), 19 | fs = require('fs'), 20 | defer; 21 | 22 | /** 23 | * Set the node env to test if it hasn't already been set 24 | */ 25 | 26 | if( !process.env.NODE_ENV ) process.env.NODE_ENV = 'test'; 27 | 28 | /** 29 | * Setup the regex which is used to match test files. 30 | * Adjust it to include coffeescript files if CS is available 31 | */ 32 | var file_matcher = /\.js$/; 33 | try { 34 | require('coffee-script'); 35 | file_matcher = /\.(js|coffee)$/; 36 | } catch (e) {} 37 | 38 | /** 39 | * Expresso version. 40 | */ 41 | 42 | var version = '0.9.2'; 43 | 44 | /** 45 | * Failure count. 46 | */ 47 | 48 | var failures = 0; 49 | 50 | /** 51 | * Indicates whether all test files have been loaded. 52 | */ 53 | 54 | var suites = 0; 55 | var suitesRun = 0; 56 | 57 | /** 58 | * Number of tests executed. 59 | */ 60 | 61 | var testcount = 0; 62 | 63 | /** 64 | * Whitelist of tests to run. 65 | */ 66 | 67 | var only = []; 68 | 69 | /** 70 | * Regex expression filtering tests to run. 71 | */ 72 | 73 | var match = ''; 74 | 75 | /** 76 | * Boring output. 77 | */ 78 | 79 | var boring = false; 80 | 81 | /** 82 | * Growl notifications. 83 | */ 84 | 85 | var growl = false; 86 | 87 | /** 88 | * Server port. 89 | */ 90 | 91 | var port = 5555; 92 | 93 | /** 94 | * Execute serially. 95 | */ 96 | 97 | var serial = false; 98 | 99 | /** 100 | * Default timeout. 101 | */ 102 | 103 | var timeout = 2000; 104 | 105 | /** 106 | * Quiet output. 107 | */ 108 | 109 | var quiet = false; 110 | 111 | /** 112 | * JSON code coverage report 113 | */ 114 | var jsonCoverage = false; 115 | var jsonFile; 116 | 117 | /** 118 | * Usage documentation. 119 | */ 120 | 121 | var usage = '' 122 | + '[bold]{Usage}: expresso [options] ' 123 | + '\n' 124 | + '\n[bold]{Options}:' 125 | + '\n -g, --growl Enable growl notifications' 126 | //+ '\n -c, --coverage Generate and report test coverage' 127 | + '\n -j, --json PATH Used in conjunction with --coverage, ouput JSON coverage to PATH' 128 | + '\n -q, --quiet Suppress coverage report if 100%' 129 | + '\n -t, --timeout MS Timeout in milliseconds, defaults to 2000' 130 | + '\n -r, --require PATH Require the given module path' 131 | + '\n -o, --only TESTS Execute only the comma sperated TESTS (can be set several times)' 132 | + '\n -m, --match EXP Execute only tests matching a given regular expression (can be set several times)' 133 | + '\n -p, --port NUM Port number for test servers, starts at 5555' 134 | + '\n -s, --serial Execute tests serially' 135 | + '\n -b, --boring Suppress ansi-escape colors' 136 | + '\n -v, --version Output version number' 137 | + '\n -h, --help Display help information' 138 | + '\n'; 139 | 140 | // Parse arguments 141 | 142 | var files = [], 143 | args = process.argv.slice(2); 144 | 145 | while (args.length) { 146 | var arg = args.shift(); 147 | switch (arg) { 148 | case '-h': 149 | case '--help': 150 | print(usage + '\n'); 151 | process.exit(1); 152 | break; 153 | case '-v': 154 | case '--version': 155 | console.log(version); 156 | process.exit(1); 157 | break; 158 | case '-o': 159 | case '--only': 160 | if (arg = args.shift()) { 161 | only = only.concat(arg.split(/ *, */)); 162 | } else { 163 | throw new Error('--only requires comma-separated test names'); 164 | } 165 | break; 166 | case '-m': 167 | case '--match': 168 | if (arg = args.shift()) { 169 | match += (match.length > 0 ? '|' : '') + arg; 170 | } else { 171 | throw new Error('--match requires argument'); 172 | } 173 | break; 174 | case '-p': 175 | case '--port': 176 | if (arg = args.shift()) { 177 | port = parseInt(arg, 10); 178 | } else { 179 | throw new Error('--port requires a number'); 180 | } 181 | break; 182 | case '-r': 183 | case '--require': 184 | if (arg = args.shift()) { 185 | require(arg); 186 | } else { 187 | throw new Error('--require requires a path'); 188 | } 189 | break; 190 | case '-t': 191 | case '--timeout': 192 | if (arg = args.shift()) { 193 | timeout = parseInt(arg, 10); 194 | } else { 195 | throw new Error('--timeout requires an argument'); 196 | } 197 | break; 198 | // case '-c': 199 | // case '--cov': 200 | // case '--coverage': 201 | // defer = true; 202 | // childProcess.exec('rm -fr lib-cov && node-jscoverage lib lib-cov', function(err){ 203 | // if (err) throw err; 204 | // require.paths.unshift('lib-cov'); 205 | // run(files); 206 | // }) 207 | // break; 208 | case '-q': 209 | case '--quiet': 210 | quiet = true; 211 | break; 212 | case '-b': 213 | case '--boring': 214 | boring = true; 215 | break; 216 | case '-g': 217 | case '--growl': 218 | growl = true; 219 | break; 220 | case '-s': 221 | case '--serial': 222 | serial = true; 223 | break; 224 | case '-j': 225 | case '--json': 226 | jsonCoverage = true; 227 | if (arg = args.shift()) { 228 | jsonFile = path.normalize(arg); 229 | } else { 230 | throw new Error('--json requires file to write to'); 231 | } 232 | break; 233 | default: 234 | if (file_matcher.test(arg)) { 235 | files.push(arg); 236 | } 237 | break; 238 | } 239 | } 240 | 241 | /** 242 | * Colorized console.error(). 243 | * 244 | * @param {String} str 245 | */ 246 | 247 | function print(str){ 248 | console.error(colorize(str)); 249 | } 250 | 251 | /** 252 | * Colorize the given string using ansi-escape sequences. 253 | * Disabled when --boring is set. 254 | * 255 | * @param {String} str 256 | * @return {String} 257 | */ 258 | 259 | function colorize(str) { 260 | var colors = { bold: 1, red: 31, green: 32, yellow: 33 }; 261 | return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str) { 262 | return boring 263 | ? str 264 | : '\x1B[' + colors[color] + 'm' + str + '\x1B[0m'; 265 | }); 266 | } 267 | 268 | // Alias deepEqual as eql for complex equality 269 | 270 | assert.eql = assert.deepEqual; 271 | 272 | /** 273 | * Assert that `val` is null. 274 | * 275 | * @param {Mixed} val 276 | * @param {String} msg 277 | */ 278 | 279 | assert.isNull = function(val, msg) { 280 | assert.strictEqual(null, val, msg); 281 | }; 282 | 283 | /** 284 | * Assert that `val` is not null. 285 | * 286 | * @param {Mixed} val 287 | * @param {String} msg 288 | */ 289 | 290 | assert.isNotNull = function(val, msg) { 291 | assert.notStrictEqual(null, val, msg); 292 | }; 293 | 294 | /** 295 | * Assert that `val` is undefined. 296 | * 297 | * @param {Mixed} val 298 | * @param {String} msg 299 | */ 300 | 301 | assert.isUndefined = function(val, msg) { 302 | assert.strictEqual(undefined, val, msg); 303 | }; 304 | 305 | /** 306 | * Assert that `val` is not undefined. 307 | * 308 | * @param {Mixed} val 309 | * @param {String} msg 310 | */ 311 | 312 | assert.isDefined = function(val, msg) { 313 | assert.notStrictEqual(undefined, val, msg); 314 | }; 315 | 316 | /** 317 | * Assert that `obj` is `type`. 318 | * 319 | * @param {Mixed} obj 320 | * @param {String} type 321 | * @api public 322 | */ 323 | 324 | assert.type = function(obj, type, msg) { 325 | var real = typeof obj; 326 | msg = msg || 'typeof ' + util.inspect(obj) + ' is ' + real + ', expected ' + type; 327 | assert.ok(type === real, msg); 328 | }; 329 | 330 | /** 331 | * Assert that `str` matches `regexp`. 332 | * 333 | * @param {String} str 334 | * @param {RegExp} regexp 335 | * @param {String} msg 336 | */ 337 | 338 | assert.match = function(str, regexp, msg) { 339 | msg = msg || util.inspect(str) + ' does not match ' + util.inspect(regexp); 340 | assert.ok(regexp.test(str), msg); 341 | }; 342 | 343 | /** 344 | * Assert that `val` is within `obj`. 345 | * 346 | * Examples: 347 | * 348 | * assert.includes('foobar', 'bar'); 349 | * assert.includes(['foo', 'bar'], 'foo'); 350 | * 351 | * @param {String|Array} obj 352 | * @param {Mixed} val 353 | * @param {String} msg 354 | */ 355 | 356 | assert.includes = function(obj, val, msg) { 357 | msg = msg || util.inspect(obj) + ' does not include ' + util.inspect(val); 358 | assert.ok(obj.indexOf(val) >= 0, msg); 359 | }; 360 | 361 | /** 362 | * Assert length of `val` is `n`. 363 | * 364 | * @param {Mixed} val 365 | * @param {Number} n 366 | * @param {String} msg 367 | */ 368 | 369 | assert.length = function(val, n, msg) { 370 | msg = msg || util.inspect(val) + ' has length of ' + val.length + ', expected ' + n; 371 | assert.equal(n, val.length, msg); 372 | }; 373 | 374 | /** 375 | * Assert response from `server` with 376 | * the given `req` object and `res` assertions object. 377 | * 378 | * @param {Server} server 379 | * @param {Object} req 380 | * @param {Object|Function} res 381 | * @param {String} msg 382 | */ 383 | assert.response = function(server, req, res, msg) { 384 | var test = assert._test; 385 | 386 | // Callback as third or fourth arg 387 | var callback = typeof res === 'function' 388 | ? res 389 | : typeof msg === 'function' 390 | ? msg 391 | : function() {}; 392 | 393 | // Default messate to test title 394 | if (typeof msg === 'function') msg = null; 395 | msg = msg || test.title; 396 | msg += '. '; 397 | 398 | // Add a unique token for this call to assert.response(). We'll move it to 399 | // succeeded/failed when done 400 | var token = new Error('Response not completed: ' + msg); 401 | test._pending.push(token); 402 | 403 | function check() { 404 | try { 405 | server.__port = server.address().port; 406 | server.__listening = true; 407 | } catch (err) { 408 | process.nextTick(check); 409 | return; 410 | } 411 | if (server.__deferred) { 412 | server.__deferred.forEach(function(fn) { fn(); }); 413 | server.__deferred = null; 414 | } 415 | } 416 | 417 | // Pending responses 418 | server.setMaxListeners(99999); 419 | server.__pending = server.__pending || 0; 420 | server.__pending++; 421 | 422 | // Check that the server is ready or defer 423 | if (!server.fd) { 424 | server.__deferred = server.__deferred || []; 425 | server.listen(server.__port = port++, '127.0.0.1', check); 426 | } else if (!server.__port) { 427 | server.__deferred = server.__deferred || []; 428 | process.nextTick(check); 429 | } 430 | 431 | // The socket was created but is not yet listening, so keep deferring 432 | if (!server.__listening) { 433 | server.__deferred.push(issue); 434 | return; 435 | } else { 436 | issue(); 437 | } 438 | 439 | function issue() { 440 | // Issue request 441 | var timer, 442 | method = req.method || 'GET', 443 | status = res.status || res.statusCode, 444 | data = req.data || req.body, 445 | requestTimeout = req.timeout || 0, 446 | encoding = req.encoding || 'utf8'; 447 | 448 | var request = http.request({ 449 | host: '127.0.0.1', 450 | port: server.__port, 451 | path: req.url, 452 | method: method, 453 | headers: req.headers 454 | }); 455 | 456 | var check = function() { 457 | if (--server.__pending === 0) { 458 | server.close(); 459 | server.__listening = false; 460 | } 461 | }; 462 | 463 | // Timeout 464 | if (requestTimeout) { 465 | timer = setTimeout(function() { 466 | check(); 467 | delete req.timeout; 468 | test.failure(new Error(msg + 'Request timed out after ' + requestTimeout + 'ms.')); 469 | }, requestTimeout); 470 | } 471 | 472 | if (data) request.write(data); 473 | 474 | request.on('response', function(response) { 475 | response.body = ''; 476 | response.setEncoding(encoding); 477 | response.on('data', function(chunk) { response.body += chunk; }); 478 | response.on('end', function() { 479 | if (timer) clearTimeout(timer); 480 | try { 481 | // Assert response body 482 | if (res.body !== undefined) { 483 | var eql = res.body instanceof RegExp 484 | ? res.body.test(response.body) 485 | : res.body === response.body; 486 | assert.ok( 487 | eql, 488 | msg + 'Invalid response body.\n' 489 | + ' Expected: ' + util.inspect(res.body) + '\n' 490 | + ' Got: ' + util.inspect(response.body) 491 | ); 492 | } 493 | 494 | // Assert response status 495 | if (typeof status === 'number') { 496 | assert.equal( 497 | response.statusCode, 498 | status, 499 | msg + colorize('Invalid response status code.\n' 500 | + ' Expected: [green]{' + status + '}\n' 501 | + ' Got: [red]{' + response.statusCode + '}') 502 | ); 503 | } 504 | 505 | // Assert response headers 506 | if (res.headers) { 507 | var keys = Object.keys(res.headers); 508 | for (var i = 0, len = keys.length; i < len; ++i) { 509 | var name = keys[i], 510 | actual = response.headers[name.toLowerCase()], 511 | expected = res.headers[name], 512 | eql = expected instanceof RegExp 513 | ? expected.test(actual) 514 | : expected == actual; 515 | assert.ok( 516 | eql, 517 | msg + colorize('Invalid response header [bold]{' + name + '}.\n' 518 | + ' Expected: [green]{' + expected + '}\n' 519 | + ' Got: [red]{' + actual + '}') 520 | ); 521 | } 522 | } 523 | 524 | // Callback 525 | callback(response); 526 | 527 | // Add this to the succeeded bin. 528 | test.success(msg); 529 | } catch (err) { 530 | test.failure(err); 531 | test.callback(); 532 | } finally { 533 | // Remove our token. 534 | var idx = test._pending.indexOf(token); 535 | if (idx >= 0) { 536 | test._pending.splice(idx, 1); 537 | } else { 538 | // Someone else took our token. This is an error. 539 | test.failure(new Error('Request succeeded, but token vanished: ' + msg)); 540 | } 541 | 542 | // Potentially shut down the server. 543 | check(); 544 | } 545 | }); 546 | }); 547 | 548 | request.end(); 549 | } 550 | }; 551 | 552 | /** 553 | * Custom assert module that tacks on the current test to every error 554 | */ 555 | var customAssert = { __proto__: assert }; 556 | Object.keys(assert).forEach(function (method) { 557 | customAssert[method] = function () { 558 | try { 559 | return assert[method].apply(assert, arguments); 560 | } catch (err) { 561 | // 'this' is this assert object from the test 562 | if (this._test) { 563 | err._test = this._test; 564 | } 565 | throw err; 566 | } 567 | }; 568 | }); 569 | 570 | /** 571 | * Pad the given string to the maximum width provided. 572 | * 573 | * @param {String} str 574 | * @param {Number} width 575 | * @return {String} 576 | */ 577 | 578 | function lpad(str, width) { 579 | str = String(str); 580 | var n = width - str.length; 581 | if (n < 1) return str; 582 | while (n--) str = ' ' + str; 583 | return str; 584 | } 585 | 586 | /** 587 | * Pad the given string to the maximum width provided. 588 | * 589 | * @param {String} str 590 | * @param {Number} width 591 | * @return {String} 592 | */ 593 | 594 | function rpad(str, width) { 595 | str = String(str); 596 | var n = width - str.length; 597 | if (n < 1) return str; 598 | while (n--) str = str + ' '; 599 | return str; 600 | } 601 | 602 | /** 603 | * Report test coverage in tabular format 604 | * 605 | * @param {Object} cov 606 | */ 607 | 608 | function reportCoverageTable(cov) { 609 | // Stats 610 | print('\n [bold]{Test Coverage}\n'); 611 | var sep = ' +------------------------------------------+----------+------+------+--------+', 612 | lastSep = ' +----------+------+------+--------+'; 613 | console.log(sep); 614 | console.log(' | filename | coverage | LOC | SLOC | missed |'); 615 | console.log(sep); 616 | for (var name in cov) { 617 | var file = cov[name]; 618 | if (Array.isArray(file)) { 619 | process.stdout.write(' | ' + rpad(name, 40)); 620 | process.stdout.write(' | ' + lpad(file.coverage.toFixed(2), 8)); 621 | process.stdout.write(' | ' + lpad(file.LOC, 4)); 622 | process.stdout.write(' | ' + lpad(file.SLOC, 4)); 623 | process.stdout.write(' | ' + lpad(file.totalMisses, 6)); 624 | process.stdout.write(' |\n'); 625 | } 626 | } 627 | console.log(sep); 628 | process.stdout.write(' ' + rpad('', 40)); 629 | process.stdout.write(' | ' + lpad(cov.coverage.toFixed(2), 8)); 630 | process.stdout.write(' | ' + lpad(cov.LOC, 4)); 631 | process.stdout.write(' | ' + lpad(cov.SLOC, 4)); 632 | process.stdout.write(' | ' + lpad(cov.totalMisses, 6)); 633 | process.stdout.write(' |\n'); 634 | console.log(lastSep); 635 | // Source 636 | for (var name in cov) { 637 | if (name.match(file_matcher)) { 638 | var file = cov[name]; 639 | if ((file.coverage < 100) || !quiet) { 640 | print('\n [bold]{' + name + '}:'); 641 | print(file.source); 642 | process.stdout.write('\n'); 643 | } 644 | } 645 | } 646 | } 647 | 648 | /** 649 | * Report test coverage in raw json format 650 | * 651 | * @param {Object} cov 652 | */ 653 | 654 | function reportCoverageJson(cov) { 655 | var report = { 656 | "coverage" : cov.coverage.toFixed(2), 657 | "LOC" : cov.LOC, 658 | "SLOC" : cov.SLOC, 659 | "totalMisses" : cov.totalMisses, 660 | "files" : {} 661 | }; 662 | 663 | for (var name in cov) { 664 | var file = cov[name]; 665 | if (Array.isArray(file)) { 666 | report.files[name] = { 667 | "coverage" : file.coverage.toFixed(2), 668 | "LOC" : file.LOC, 669 | "SLOC" : file.SLOC, 670 | "totalMisses" : file.totalMisses 671 | }; 672 | } 673 | } 674 | 675 | fs.writeFileSync(jsonFile, JSON.stringify(report), "utf8"); 676 | } 677 | 678 | /** 679 | * Populate code coverage data. 680 | * 681 | * @param {Object} cov 682 | */ 683 | 684 | function populateCoverage(cov) { 685 | cov.LOC = 686 | cov.SLOC = 687 | cov.totalFiles = 688 | cov.totalHits = 689 | cov.totalMisses = 690 | cov.coverage = 0; 691 | for (var name in cov) { 692 | var file = cov[name]; 693 | if (Array.isArray(file)) { 694 | // Stats 695 | ++cov.totalFiles; 696 | cov.totalHits += file.totalHits = coverage(file, true); 697 | cov.totalMisses += file.totalMisses = coverage(file, false); 698 | file.totalLines = file.totalHits + file.totalMisses; 699 | cov.SLOC += file.SLOC = file.totalLines; 700 | if (!file.source) file.source = []; 701 | cov.LOC += file.LOC = file.source.length; 702 | file.coverage = (file.totalHits / file.totalLines) * 100; 703 | // Source 704 | var width = file.source.length.toString().length; 705 | file.source = file.source.map(function(line, i) { 706 | ++i; 707 | var hits = file[i] === 0 ? 0 : (file[i] || ' '); 708 | if (!boring) { 709 | if (hits === 0) { 710 | hits = '\x1b[31m' + hits + '\x1b[0m'; 711 | line = '\x1b[41m' + line + '\x1b[0m'; 712 | } else { 713 | hits = '\x1b[32m' + hits + '\x1b[0m'; 714 | } 715 | } 716 | return '\n ' + lpad(i, width) + ' | ' + hits + ' | ' + line; 717 | }).join(''); 718 | } 719 | } 720 | cov.coverage = (cov.totalHits / cov.SLOC) * 100; 721 | } 722 | 723 | /** 724 | * Total coverage for the given file data. 725 | * 726 | * @param {Array} data 727 | * @return {Type} 728 | */ 729 | 730 | function coverage(data, val) { 731 | var n = 0; 732 | for (var i = 0, len = data.length; i < len; ++i) { 733 | if (data[i] !== undefined && data[i] == val) ++n; 734 | } 735 | return n; 736 | } 737 | 738 | /** 739 | * Test if all files have 100% coverage 740 | * 741 | * @param {Object} cov 742 | * @return {Boolean} 743 | */ 744 | 745 | function hasFullCoverage(cov) { 746 | for (var name in cov) { 747 | var file = cov[name]; 748 | if (file instanceof Array) { 749 | if (file.coverage !== 100) { 750 | return false; 751 | } 752 | } 753 | } 754 | return true; 755 | } 756 | 757 | /** 758 | * Run the given test `files`, or try _test/*_. 759 | * 760 | * @param {Array} files 761 | */ 762 | 763 | function run(files) { 764 | cursor(false); 765 | if (!files.length) { 766 | try { 767 | files = fs.readdirSync('test').map(function(file) { 768 | return 'test/' + file; 769 | }).filter(function(file) { 770 | return !(/(^\.)|(\/\.)/.test(file)); 771 | }); 772 | } catch (err) { 773 | print('\n failed to load tests in [bold]{./test}\n'); 774 | ++failures; 775 | process.exit(1); 776 | } 777 | } 778 | runFiles(files); 779 | } 780 | 781 | /** 782 | * Show the cursor when `show` is true, otherwise hide it. 783 | * 784 | * @param {Boolean} show 785 | */ 786 | 787 | function cursor(show) { 788 | if (boring) return; 789 | if (show) { 790 | process.stdout.write('\x1b[?25h'); 791 | } else { 792 | process.stdout.write('\x1b[?25l'); 793 | } 794 | } 795 | 796 | /** 797 | * Run the given test `files`. 798 | * 799 | * @param {Array} files 800 | */ 801 | 802 | function runFiles(files) { 803 | files = files.filter(function(file) { 804 | return file.match(file_matcher); 805 | }); 806 | suites = files.length; 807 | 808 | if (serial) { 809 | (function next() { 810 | if (files.length) { 811 | runFile(files.shift(), next); 812 | } 813 | })(); 814 | } else { 815 | files.forEach(runFile); 816 | } 817 | } 818 | 819 | /** 820 | * Run tests for the given `file`, callback `fn()` when finished. 821 | * 822 | * @param {String} file 823 | * @param {Function} fn 824 | */ 825 | 826 | function runFile(file, fn) { 827 | var title = path.basename(file), 828 | file = path.join(cwd, file), 829 | mod = require(file.replace(file_matcher,'')); 830 | (function check() { 831 | var len = Object.keys(mod).length; 832 | if (len) { 833 | runSuite(title, mod, fn); 834 | suitesRun++; 835 | } else { 836 | setTimeout(check, 20); 837 | } 838 | })(); 839 | } 840 | 841 | /** 842 | * Run the given tests, callback `fn()` when finished. 843 | * 844 | * @param {String} title 845 | * @param {Object} tests 846 | * @param {Function} fn 847 | */ 848 | 849 | var dots = 0; 850 | function runSuite(title, tests, callback) { 851 | // Keys 852 | var keys = only.length 853 | ? only.slice(0) 854 | : Object.keys(tests); 855 | 856 | // Regular expression test filter 857 | var filter = new RegExp('(?:' + (match.length == 0 ? '.' : match) + ')'); 858 | 859 | // Setup 860 | var setup = tests.setup || function(fn, assert) { fn(); }; 861 | var teardown = tests.teardown || function(fn, assert) { fn(); }; 862 | 863 | process.setMaxListeners(10 + process.listeners('beforeExit').length + keys.length); 864 | 865 | // Iterate tests 866 | (function next() { 867 | if (keys.length) { 868 | var key, 869 | fn = tests[key = keys.shift()]; 870 | 871 | // Filter 872 | if (filter.test(key) === false) return next(); 873 | 874 | // Non-tests 875 | if (key === 'setup' || key === 'teardown') return next(); 876 | 877 | // Run test 878 | if (fn) { 879 | var test = new Test({ 880 | fn: fn, 881 | suite: title, 882 | title: key, 883 | setup: setup, 884 | teardown: teardown 885 | }) 886 | test.run(next); 887 | } else { 888 | // @TODO: Add warning message that there's no test. 889 | next(); 890 | } 891 | } else if (serial) { 892 | callback(); 893 | } 894 | })(); 895 | } 896 | 897 | require('util').inherits(Test, require('events').EventEmitter); 898 | function Test(options) { 899 | for (var key in options) { 900 | this[key] = options[key]; 901 | } 902 | this._succeeded = []; 903 | this._failed = []; 904 | this._pending = []; 905 | this._beforeExit = []; 906 | this.assert = { __proto__: customAssert, _test: this }; 907 | 908 | var test = this; 909 | process.on('beforeExit', function() { 910 | try { 911 | test.emit('exit'); 912 | } catch (err) { 913 | test.failure(err); 914 | } 915 | test.report(); 916 | }); 917 | } 918 | 919 | Test.prototype.success = function(err) { 920 | this._succeeded.push(err); 921 | }; 922 | 923 | Test.prototype.failure = function(err) { 924 | this._failed.push(err); 925 | this.error(err); 926 | }; 927 | 928 | Test.prototype.report = function() { 929 | for (var i = 0; i < this._pending.length; i++) { 930 | this.error(this._pending[i]); 931 | } 932 | }; 933 | 934 | Test.prototype.run = function(callback) { 935 | try { 936 | ++testcount; 937 | assert._test = this; 938 | 939 | if (serial) { 940 | this.runSerial(callback); 941 | } else { 942 | // @TODO: find a way to run setup/tearDown. 943 | this.runParallel(); 944 | callback(); 945 | } 946 | } catch (err) { 947 | this.failure(err); 948 | this.report(); 949 | } 950 | }; 951 | 952 | Test.prototype.runSerial = function(callback) { 953 | serialTest = this; 954 | 955 | var test = this; 956 | process.stdout.write('.'); 957 | if (++dots % 25 === 0) console.log(); 958 | test.setup(function() { 959 | if (test.fn.length < 1) { 960 | test.fn(); 961 | test.teardown(callback); 962 | } else { 963 | var id = setTimeout(function() { 964 | throw new Error("'" + test.title + "' timed out"); 965 | }, timeout); 966 | test.callback = function() { 967 | clearTimeout(id); 968 | test.teardown(callback); 969 | process.nextTick(function() { 970 | test.report(); 971 | }); 972 | }; 973 | test.fn(test.callback); 974 | } 975 | }); 976 | }; 977 | 978 | Test.prototype.runParallel = function() { 979 | var test = this; 980 | test.fn(function(fn) { 981 | test.on('exit', function() { 982 | fn(test.assert); 983 | }); 984 | }, test.assert); 985 | }; 986 | 987 | /** 988 | * Report `err` for the given `test` and `suite`. 989 | * 990 | * @param {String} suite 991 | * @param {String} test 992 | * @param {Error} err 993 | */ 994 | Test.prototype.error = function(err) { 995 | if (!err._reported) { 996 | ++failures; 997 | var test = err._test || this; 998 | var name = err.name, 999 | stack = err.stack ? err.stack.replace(err.name, '') : '', 1000 | label = test.suite + ' ' + test.title; 1001 | print('\n [bold]{' + label + '}: [red]{' + name + '}' + stack + '\n'); 1002 | err._reported = true; 1003 | } 1004 | } 1005 | 1006 | /** 1007 | * Report exceptions. 1008 | */ 1009 | 1010 | function report() { 1011 | cursor(true); 1012 | process.emit('beforeExit'); 1013 | if (suitesRun < suites) { 1014 | print('\n [bold]{Failure}: [red]{Only ' + suitesRun + ' of ' + suites + ' suites have been started}\n\n'); 1015 | } 1016 | else if (failures) { 1017 | print('\n [bold]{Failures}: [red]{' + failures + '}\n\n'); 1018 | notify('Failures: ' + failures); 1019 | } else { 1020 | if (serial) print(''); 1021 | print('\n [green]{100%} ' + testcount + ' tests\n'); 1022 | notify('100% ok'); 1023 | } 1024 | if (typeof _$jscoverage === 'object') { 1025 | populateCoverage(_$jscoverage); 1026 | if (!hasFullCoverage(_$jscoverage) || !quiet) { 1027 | (jsonCoverage ? reportCoverageJson(_$jscoverage) : reportCoverageTable(_$jscoverage)); 1028 | } 1029 | } 1030 | } 1031 | 1032 | /** 1033 | * Growl notify the given `msg`. 1034 | * 1035 | * @param {String} msg 1036 | */ 1037 | 1038 | function notify(msg) { 1039 | if (growl) { 1040 | childProcess.exec('growlnotify -name Expresso -m "' + msg + '"'); 1041 | } 1042 | } 1043 | 1044 | // Report uncaught exceptions 1045 | var unknownTest = new Test({ 1046 | suite: 'uncaught', 1047 | test: 'uncaught' 1048 | }); 1049 | var serialTest; 1050 | 1051 | process.on('uncaughtException', function(err) { 1052 | if (!serial) { 1053 | unknownTest.error(err); 1054 | } else { 1055 | serialTest.failure(err); 1056 | serialTest.callback(); 1057 | } 1058 | }); 1059 | 1060 | // Show cursor 1061 | 1062 | ['INT', 'TERM', 'QUIT'].forEach(function(sig) { 1063 | process.on('SIG' + sig, function() { 1064 | cursor(true); 1065 | process.exit(1); 1066 | }); 1067 | }); 1068 | 1069 | // Report test coverage when available 1070 | // and emit "beforeExit" event to perform 1071 | // final assertions 1072 | 1073 | var orig = process.emit; 1074 | process.emit = function(event) { 1075 | if (event === 'exit') { 1076 | report(); 1077 | process.reallyExit(failures); 1078 | } 1079 | orig.apply(this, arguments); 1080 | }; 1081 | 1082 | // Run test files 1083 | 1084 | if (!defer) run(files); 1085 | -------------------------------------------------------------------------------- /docs/api.html: -------------------------------------------------------------------------------- 1 | Fork me on GitHub 2 | 3 | Expresso 4 | 5 | 99 | 109 | 110 | 111 | 112 | 115 | 122 | 123 | 124 | 128 | 138 | 139 | 140 | 144 | 147 | 148 | 149 | 154 | 161 | 162 | 163 | 167 | 170 | 171 | 172 | 176 | 179 | 180 | 181 | 185 | 189 | 190 | 191 | 195 | 198 | 199 | 200 | 204 | 207 | 208 | 209 | 213 | 216 | 217 | 218 | 222 | 225 | 226 | 227 | 231 | 234 | 235 | 236 | 240 | 243 | 244 | 245 | 249 | 252 | 253 | 254 | 258 | 261 | 262 | 263 | 267 | 271 | 272 | 273 | 277 | 399 | 400 | 401 | 408 | 413 | 414 | 415 | 423 | 437 | 438 | 439 | 446 | 451 | 452 | 453 | 460 | 465 | 466 | 467 | 474 | 479 | 480 | 481 | 488 | 493 | 494 | 495 | 502 | 509 | 510 | 511 | 518 | 524 | 525 | 526 | 538 | 544 | 545 | 546 | 553 | 559 | 560 | 561 | 569 | 738 | 739 | 740 | 747 | 756 | 757 | 758 | 765 | 774 | 775 | 776 | 783 | 824 | 825 | 826 | 833 | 858 | 859 | 860 | 867 | 907 | 908 | 909 | 916 | 925 | 926 | 927 | 934 | 947 | 948 | 949 | 956 | 975 | 976 | 977 | 984 | 994 | 995 | 996 | 1003 | 1021 | 1022 | 1023 | 1030 | 1046 | 1047 | 1048 | 1055 | 1185 | 1186 | 1187 | 1194 | 1208 | 1209 | 1210 | 1214 | 1237 | 1238 | 1239 | 1246 | 1290 | 1291 |

Expresso

Insanely fast TDD framework for node featuring code coverage reporting.

expresso

bin/expresso
113 |

!/usr/bin/env node

114 |
116 |

 117 |  * Expresso
 118 |  * Copyright(c) TJ Holowaychuk &lt;tj@vision-media.ca&gt;
 119 |  * (MIT Licensed)
 120 |  
121 |
125 |

Module dependencies. 126 |

127 |
129 |
var assert = require('assert'),
 130 |     childProcess = require('child_process'),
 131 |     http = require('http'),
 132 |     path = require('path'),
 133 |     sys = require('sys'),
 134 |     cwd = process.cwd(),
 135 |     fs = require('fs'),
 136 |     defer;
137 |
141 |

Set the node env to test if it hasn't already been set 142 |

143 |
145 |
if( !process.env.NODE_ENV ) process.env.NODE_ENV = 'test';
146 |
150 |

Setup the regex which is used to match test files. 151 | Adjust it to include coffeescript files if CS is available 152 |

153 |
155 |
var file_matcher = /\.js$/;
 156 | try {
 157 |   require('coffee-script');
 158 |   file_matcher = /\.(js|coffee)$/;
 159 | } catch (e) {}
160 |
164 |

Expresso version. 165 |

166 |
168 |
var version = '0.8.1';
169 |
173 |

Failure count. 174 |

175 |
177 |
var failures = 0;
178 |
182 |

Indicates whether all test files have been loaded. 183 |

184 |
186 |
var suites = 0;
 187 | var suitesRun = 0;
188 |
192 |

Number of tests executed. 193 |

194 |
196 |
var testcount = 0;
197 |
201 |

Whitelist of tests to run. 202 |

203 |
205 |
var only = [];
206 |
210 |

Boring output. 211 |

212 |
214 |
var boring = false;
215 |
219 |

Growl notifications. 220 |

221 |
223 |
var growl = false;
224 |
228 |

Server port. 229 |

230 |
232 |
var port = 5555;
233 |
237 |

Execute serially. 238 |

239 |
241 |
var serial = false;
242 |
246 |

Default timeout. 247 |

248 |
250 |
var timeout = 2000;
251 |
255 |

Quiet output. 256 |

257 |
259 |
var quiet = false;
260 |
264 |

JSON code coverage report 265 |

266 |
268 |
var jsonCoverage = false;
 269 | var jsonFile;
270 |
274 |

Usage documentation. 275 |

276 |
278 |
var usage = ''
 279 |     + '[bold]{Usage}: expresso [options] <file ...>'
 280 |     + '\n'
 281 |     + '\n[bold]{Options}:'
 282 |     + '\n  -g, --growl          Enable growl notifications'
 283 |     + '\n  -c, --coverage       Generate and report test coverage'
 284 |     + '\n  -j, --json PATH      Used in conjunction with --coverage, ouput JSON coverage to PATH'
 285 |     + '\n  -q, --quiet          Suppress coverage report if 100%'
 286 |     + '\n  -t, --timeout MS     Timeout in milliseconds, defaults to 2000'
 287 |     + '\n  -r, --require PATH   Require the given module path'
 288 |     + '\n  -o, --only TESTS     Execute only the comma sperated TESTS (can be set several times)'
 289 |     + '\n  -I, --include PATH   Unshift the given path to require.paths'
 290 |     + '\n  -p, --port NUM       Port number for test servers, starts at 5555'
 291 |     + '\n  -s, --serial         Execute tests serially'
 292 |     + '\n  -b, --boring         Suppress ansi-escape colors'
 293 |     + '\n  -v, --version        Output version number'
 294 |     + '\n  -h, --help           Display help information'
 295 |     + '\n';
 296 | 
 297 | // Parse arguments
 298 | 
 299 | var files = [],
 300 |     args = process.argv.slice(2);
 301 | 
 302 | while (args.length) {
 303 |     var arg = args.shift();
 304 |     switch (arg) {
 305 |         case '-h':
 306 |         case '--help':
 307 |             print(usage + '\n');
 308 |             process.exit(1);
 309 |             break;
 310 |         case '-v':
 311 |         case '--version':
 312 |             sys.puts(version);
 313 |             process.exit(1);
 314 |             break;
 315 |         case '-i':
 316 |         case '-I':
 317 |         case '--include':
 318 |             if (arg = args.shift()) {
 319 |                 require.paths.unshift(arg);
 320 |             } else {
 321 |                 throw new Error('--include requires a path');
 322 |             }
 323 |             break;
 324 |         case '-o':
 325 |         case '--only':
 326 |             if (arg = args.shift()) {
 327 |                 only = only.concat(arg.split(/ *, */));
 328 |             } else {
 329 |                 throw new Error('--only requires comma-separated test names');
 330 |             }
 331 |             break;
 332 |         case '-p':
 333 |         case '--port':
 334 |             if (arg = args.shift()) {
 335 |                 port = parseInt(arg, 10);
 336 |             } else {
 337 |                 throw new Error('--port requires a number');
 338 |             }
 339 |             break;
 340 |         case '-r':
 341 |         case '--require':
 342 |             if (arg = args.shift()) {
 343 |                 require(arg);
 344 |             } else {
 345 |                 throw new Error('--require requires a path');
 346 |             }
 347 |             break;
 348 |         case '-t':
 349 |         case '--timeout':
 350 |           if (arg = args.shift()) {
 351 |             timeout = parseInt(arg, 10);
 352 |           } else {
 353 |             throw new Error('--timeout requires an argument');
 354 |           }
 355 |           break;
 356 |         case '-c':
 357 |         case '--cov':
 358 |         case '--coverage':
 359 |             defer = true;
 360 |             childProcess.exec('rm -fr lib-cov && node-jscoverage lib lib-cov', function(err) {
 361 |                 if (err) throw err;
 362 |                 require.paths.unshift('lib-cov');
 363 |                 run(files);
 364 |             });
 365 |             break;
 366 |         case '-q':
 367 |         case '--quiet':
 368 |             quiet = true;
 369 |             break;
 370 |         case '-b':
 371 |         case '--boring':
 372 |             boring = true;
 373 |             break;
 374 |         case '-g':
 375 |         case '--growl':
 376 |             growl = true;
 377 |             break;
 378 |         case '-s':
 379 |         case '--serial':
 380 |             serial = true;
 381 |             break;
 382 |         case '-j':
 383 |         case '--json':
 384 |             jsonCoverage = true;
 385 |             if (arg = args.shift()) {
 386 |                 jsonFile = path.normalize(arg);
 387 |             } else {
 388 |                 throw new Error('--json requires file to write to');
 389 |             }
 390 |             break;
 391 |         default:
 392 |             if (file_matcher.test(arg)) {
 393 |                 files.push(arg);
 394 |             }
 395 |             break;
 396 |     }
 397 | }
398 |
402 |

Colorized sys.error().

403 | 404 |

405 | 406 |
  • param: String str

407 |
409 |
function print(str) {
 410 |     sys.error(colorize(str));
 411 | }
412 |
416 |

Colorize the given string using ansi-escape sequences. 417 | Disabled when --boring is set.

418 | 419 |

420 | 421 |
  • param: String str

  • return: String

422 |
424 |
function colorize(str) {
 425 |     var colors = { bold: 1, red: 31, green: 32, yellow: 33 };
 426 |     return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str) {
 427 |         return boring
 428 |             ? str
 429 |             : '\x1B[' + colors[color] + 'm' + str + '\x1B[0m';
 430 |     });
 431 | }
 432 | 
 433 | // Alias deepEqual as eql for complex equality
 434 | 
 435 | assert.eql = assert.deepEqual;
436 |
440 |

Assert that val is null.

441 | 442 |

443 | 444 |
  • param: Mixed val

  • param: String msg

445 |
447 |
assert.isNull = function(val, msg) {
 448 |     assert.strictEqual(null, val, msg);
 449 | };
450 |
454 |

Assert that val is not null.

455 | 456 |

457 | 458 |
  • param: Mixed val

  • param: String msg

459 |
461 |
assert.isNotNull = function(val, msg) {
 462 |     assert.notStrictEqual(null, val, msg);
 463 | };
464 |
468 |

Assert that val is undefined.

469 | 470 |

471 | 472 |
  • param: Mixed val

  • param: String msg

473 |
475 |
assert.isUndefined = function(val, msg) {
 476 |     assert.strictEqual(undefined, val, msg);
 477 | };
478 |
482 |

Assert that val is not undefined.

483 | 484 |

485 | 486 |
  • param: Mixed val

  • param: String msg

487 |
489 |
assert.isDefined = function(val, msg) {
 490 |     assert.notStrictEqual(undefined, val, msg);
 491 | };
492 |
496 |

Assert that obj is type.

497 | 498 |

499 | 500 |
  • param: Mixed obj

  • param: String type

  • api: public

501 |
503 |
assert.type = function(obj, type, msg) {
 504 |     var real = typeof obj;
 505 |     msg = msg || 'typeof ' + sys.inspect(obj) + ' is ' + real + ', expected ' + type;
 506 |     assert.ok(type === real, msg);
 507 | };
508 |
512 |

Assert that str matches regexp.

513 | 514 |

515 | 516 |
  • param: String str

  • param: RegExp regexp

  • param: String msg

517 |
519 |
assert.match = function(str, regexp, msg) {
 520 |     msg = msg || sys.inspect(str) + ' does not match ' + sys.inspect(regexp);
 521 |     assert.ok(regexp.test(str), msg);
 522 | };
523 |
527 |

Assert that val is within obj.

528 | 529 |

Examples

530 | 531 |

assert.includes('foobar', 'bar'); 532 | assert.includes(['foo', 'bar'], 'foo');

533 | 534 |

535 | 536 |
  • param: String | Array obj

  • param: Mixed val

  • param: String msg

537 |
539 |
assert.includes = function(obj, val, msg) {
 540 |     msg = msg || sys.inspect(obj) + ' does not include ' + sys.inspect(val);
 541 |     assert.ok(obj.indexOf(val) &gt;= 0, msg);
 542 | };
543 |
547 |

Assert length of val is n.

548 | 549 |

550 | 551 |
  • param: Mixed val

  • param: Number n

  • param: String msg

552 |
554 |
assert.length = function(val, n, msg) {
 555 |     msg = msg || sys.inspect(val) + ' has length of ' + val.length + ', expected ' + n;
 556 |     assert.equal(n, val.length, msg);
 557 | };
558 |
562 |

Assert response from server with 563 | the given req object and res assertions object.

564 | 565 |

566 | 567 |
  • param: Server server

  • param: Object req

  • param: Object | Function res

  • param: String msg

568 |
570 |
assert.response = function(server, req, res, msg) {
 571 |     var test = assert._test;
 572 | 
 573 |     // Callback as third or fourth arg
 574 |     var callback = typeof res === 'function'
 575 |         ? res
 576 |         : typeof msg === 'function'
 577 |             ? msg
 578 |             : function() {};
 579 | 
 580 |     // Default messate to test title
 581 |     if (typeof msg === 'function') msg = null;
 582 |     msg = msg || test.title;
 583 |     msg += '. ';
 584 | 
 585 |     // Add a unique token for this call to assert.response(). We'll move it to
 586 |     // succeeded/failed when done
 587 |     var token = new Error('Response not completed: ' + msg);
 588 |     test._pending.push(token);
 589 | 
 590 |     function check() {
 591 |         try {
 592 |             server.__port = server.address().port;
 593 |             server.__listening = true;
 594 |         } catch (err) {
 595 |             process.nextTick(check);
 596 |             return;
 597 |         }
 598 |         if (server.__deferred) {
 599 |             server.__deferred.forEach(function(fn) { fn(); });
 600 |             server.__deferred = null;
 601 |         }
 602 |     }
 603 | 
 604 |     // Pending responses
 605 |     server.__pending = server.__pending || 0;
 606 |     server.__pending++;
 607 | 
 608 |     // Check that the server is ready or defer
 609 |     if (!server.fd) {
 610 |         server.__deferred = server.__deferred || [];
 611 |         server.listen(server.__port = port++, '127.0.0.1', check);
 612 |     } else if (!server.__port) {
 613 |         server.__deferred = server.__deferred || [];
 614 |         process.nextTick(check);
 615 |     }
 616 | 
 617 |     // The socket was created but is not yet listening, so keep deferring
 618 |     if (!server.__listening) {
 619 |         server.__deferred.push(issue);
 620 |         return;
 621 |     } else {
 622 |         issue();
 623 |     }
 624 | 
 625 |     function issue() {
 626 |         // Issue request
 627 |         var timer,
 628 |             method = req.method || 'GET',
 629 |             status = res.status || res.statusCode,
 630 |             data = req.data || req.body,
 631 |             requestTimeout = req.timeout || 0,
 632 |             encoding = req.encoding || 'utf8';
 633 | 
 634 |         var request = http.request({
 635 |             host: '127.0.0.1',
 636 |             port: server.__port,
 637 |             path: req.url,
 638 |             method: method,
 639 |             headers: req.headers
 640 |         });
 641 | 
 642 |         var check = function() {
 643 |             if (--server.__pending === 0) {
 644 |                 server.close();
 645 |                 server.__listening = false;
 646 |             }
 647 |         };
 648 | 
 649 |         // Timeout
 650 |         if (requestTimeout) {
 651 |             timer = setTimeout(function() {
 652 |                 check();
 653 |                 delete req.timeout;
 654 |                 test.failure(new Error(msg + 'Request timed out after ' + requestTimeout + 'ms.'));
 655 |             }, requestTimeout);
 656 |         }
 657 | 
 658 |         if (data) request.write(data);
 659 | 
 660 |         request.on('response', function(response) {
 661 |             response.body = '';
 662 |             response.setEncoding(encoding);
 663 |             response.on('data', function(chunk) { response.body += chunk; });
 664 |             response.on('end', function() {
 665 |                 if (timer) clearTimeout(timer);
 666 |                 try {
 667 |                     // Assert response body
 668 |                     if (res.body !== undefined) {
 669 |                         var eql = res.body instanceof RegExp
 670 |                           ? res.body.test(response.body)
 671 |                           : res.body === response.body;
 672 |                         assert.ok(
 673 |                             eql,
 674 |                             msg + 'Invalid response body.\n'
 675 |                                 + '    Expected: ' + sys.inspect(res.body) + '\n'
 676 |                                 + '    Got: ' + sys.inspect(response.body)
 677 |                         );
 678 |                     }
 679 | 
 680 |                     // Assert response status
 681 |                     if (typeof status === 'number') {
 682 |                         assert.equal(
 683 |                             response.statusCode,
 684 |                             status,
 685 |                             msg + colorize('Invalid response status code.\n'
 686 |                                 + '    Expected: [green]{' + status + '}\n'
 687 |                                 + '    Got: [red]{' + response.statusCode + '}')
 688 |                         );
 689 |                     }
 690 | 
 691 |                     // Assert response headers
 692 |                     if (res.headers) {
 693 |                         var keys = Object.keys(res.headers);
 694 |                         for (var i = 0, len = keys.length; i &lt; len; ++i) {
 695 |                             var name = keys[i],
 696 |                                 actual = response.headers[name.toLowerCase()],
 697 |                                 expected = res.headers[name],
 698 |                                 eql = expected instanceof RegExp
 699 |                                   ? expected.test(actual)
 700 |                                   : expected == actual;
 701 |                             assert.ok(
 702 |                                 eql,
 703 |                                 msg + colorize('Invalid response header [bold]{' + name + '}.\n'
 704 |                                     + '    Expected: [green]{' + expected + '}\n'
 705 |                                     + '    Got: [red]{' + actual + '}')
 706 |                             );
 707 |                         }
 708 |                     }
 709 | 
 710 |                     // Callback
 711 |                     callback(response);
 712 | 
 713 |                     // Add this to the succeeded bin.
 714 |                     test.success(msg);
 715 |                 } catch (err) {
 716 |                     test.failure(err);
 717 |                     test.callback();
 718 |                 } finally {
 719 |                     // Remove our token.
 720 |                     var idx = test._pending.indexOf(token);
 721 |                     if (idx &gt;= 0) {
 722 |                         test._pending.splice(idx, 1);
 723 |                     } else {
 724 |                         // Someone else took our token. This is an error.
 725 |                         test.failure(new Error('Request succeeded, but token vanished: ' + msg));
 726 |                     }
 727 | 
 728 |                     // Potentially shut down the server.
 729 |                     check();
 730 |                 }
 731 |             });
 732 |         });
 733 | 
 734 |         request.end();
 735 |       }
 736 | };
737 |
741 |

Pad the given string to the maximum width provided.

742 | 743 |

744 | 745 |
  • param: String str

  • param: Number width

  • return: String

746 |
748 |
function lpad(str, width) {
 749 |     str = String(str);
 750 |     var n = width - str.length;
 751 |     if (n &lt; 1) return str;
 752 |     while (n--) str = ' ' + str;
 753 |     return str;
 754 | }
755 |
759 |

Pad the given string to the maximum width provided.

760 | 761 |

762 | 763 |
  • param: String str

  • param: Number width

  • return: String

764 |
766 |
function rpad(str, width) {
 767 |     str = String(str);
 768 |     var n = width - str.length;
 769 |     if (n &lt; 1) return str;
 770 |     while (n--) str = str + ' ';
 771 |     return str;
 772 | }
773 |
777 |

Report test coverage in tabular format

778 | 779 |

780 | 781 |
  • param: Object cov

782 |
784 |
function reportCoverageTable(cov) {
 785 |     // Stats
 786 |     print('\n   [bold]{Test Coverage}\n');
 787 |     var sep = '   +------------------------------------------+----------+------+------+--------+',
 788 |         lastSep = '                                              +----------+------+------+--------+';
 789 |     sys.puts(sep);
 790 |     sys.puts('   | filename                                 | coverage | LOC  | SLOC | missed |');
 791 |     sys.puts(sep);
 792 |     for (var name in cov) {
 793 |         var file = cov[name];
 794 |         if (Array.isArray(file)) {
 795 |             sys.print('   | ' + rpad(name, 40));
 796 |             sys.print(' | ' + lpad(file.coverage.toFixed(2), 8));
 797 |             sys.print(' | ' + lpad(file.LOC, 4));
 798 |             sys.print(' | ' + lpad(file.SLOC, 4));
 799 |             sys.print(' | ' + lpad(file.totalMisses, 6));
 800 |             sys.print(' |\n');
 801 |         }
 802 |     }
 803 |     sys.puts(sep);
 804 |     sys.print('     ' + rpad('', 40));
 805 |     sys.print(' | ' + lpad(cov.coverage.toFixed(2), 8));
 806 |     sys.print(' | ' + lpad(cov.LOC, 4));
 807 |     sys.print(' | ' + lpad(cov.SLOC, 4));
 808 |     sys.print(' | ' + lpad(cov.totalMisses, 6));
 809 |     sys.print(' |\n');
 810 |     sys.puts(lastSep);
 811 |     // Source
 812 |     for (var name in cov) {
 813 |         if (name.match(file_matcher)) {
 814 |             var file = cov[name];
 815 |             if ((file.coverage &lt; 100) || !quiet) {
 816 |                print('\n   [bold]{' + name + '}:');
 817 |                print(file.source);
 818 |                sys.print('\n');
 819 |             }
 820 |         }
 821 |     }
 822 | }
823 |
827 |

Report test coverage in raw json format

828 | 829 |

830 | 831 |
  • param: Object cov

832 |
834 |
function reportCoverageJson(cov) {
 835 |     var report = {
 836 |         &quot;coverage&quot; : cov.coverage.toFixed(2),
 837 |         &quot;LOC&quot; : cov.LOC,
 838 |         &quot;SLOC&quot; : cov.SLOC,
 839 |         &quot;totalMisses&quot; : cov.totalMisses,
 840 |         &quot;files&quot; : {}
 841 |     };
 842 | 
 843 |     for (var name in cov) {
 844 |         var file = cov[name];
 845 |         if (Array.isArray(file)) {
 846 |             report.files[name] = {
 847 |                 &quot;coverage&quot; : file.coverage.toFixed(2),
 848 |                 &quot;LOC&quot; : file.LOC,
 849 |                 &quot;SLOC&quot; : file.SLOC,
 850 |                 &quot;totalMisses&quot; : file.totalMisses
 851 |             };
 852 |         }
 853 |     }
 854 | 
 855 |     fs.writeFileSync(jsonFile, JSON.stringify(report), &quot;utf8&quot;);
 856 | }
857 |
861 |

Populate code coverage data.

862 | 863 |

864 | 865 |
  • param: Object cov

866 |
868 |
function populateCoverage(cov) {
 869 |     cov.LOC =
 870 |     cov.SLOC =
 871 |     cov.totalFiles =
 872 |     cov.totalHits =
 873 |     cov.totalMisses =
 874 |     cov.coverage = 0;
 875 |     for (var name in cov) {
 876 |         var file = cov[name];
 877 |         if (Array.isArray(file)) {
 878 |             // Stats
 879 |             ++cov.totalFiles;
 880 |             cov.totalHits += file.totalHits = coverage(file, true);
 881 |             cov.totalMisses += file.totalMisses = coverage(file, false);
 882 |             file.totalLines = file.totalHits + file.totalMisses;
 883 |             cov.SLOC += file.SLOC = file.totalLines;
 884 |             if (!file.source) file.source = [];
 885 |             cov.LOC += file.LOC = file.source.length;
 886 |             file.coverage = (file.totalHits / file.totalLines) * 100;
 887 |             // Source
 888 |             var width = file.source.length.toString().length;
 889 |             file.source = file.source.map(function(line, i) {
 890 |                 ++i;
 891 |                 var hits = file[i] === 0 ? 0 : (file[i] || ' ');
 892 |                 if (!boring) {
 893 |                     if (hits === 0) {
 894 |                         hits = '\x1b[31m' + hits + '\x1b[0m';
 895 |                         line = '\x1b[41m' + line + '\x1b[0m';
 896 |                     } else {
 897 |                         hits = '\x1b[32m' + hits + '\x1b[0m';
 898 |                     }
 899 |                 }
 900 |                 return '\n     ' + lpad(i, width) + ' | ' + hits + ' | ' + line;
 901 |             }).join('');
 902 |         }
 903 |     }
 904 |     cov.coverage = (cov.totalHits / cov.SLOC) * 100;
 905 | }
906 |
910 |

Total coverage for the given file data.

911 | 912 |

913 | 914 |
  • param: Array data

  • return: Type

915 |
917 |
function coverage(data, val) {
 918 |     var n = 0;
 919 |     for (var i = 0, len = data.length; i &lt; len; ++i) {
 920 |         if (data[i] !== undefined &amp;&amp; data[i] == val) ++n;
 921 |     }
 922 |     return n;
 923 | }
924 |
928 |

Test if all files have 100% coverage

929 | 930 |

931 | 932 |
  • param: Object cov

  • return: Boolean

933 |
935 |
function hasFullCoverage(cov) {
 936 |   for (var name in cov) {
 937 |     var file = cov[name];
 938 |     if (file instanceof Array) {
 939 |       if (file.coverage !== 100) {
 940 |           return false;
 941 |       }
 942 |     }
 943 |   }
 944 |   return true;
 945 | }
946 |
950 |

Run the given test files, or try test/*.

951 | 952 |

953 | 954 |
  • param: Array files

955 |
957 |
function run(files) {
 958 |     cursor(false);
 959 |     if (!files.length) {
 960 |         try {
 961 |             files = fs.readdirSync('test').map(function(file) {
 962 |                 return 'test/' + file;
 963 |             }).filter(function(file) {
 964 |                 return !(/(^\.)|(\/\.)/.test(file));
 965 |             });
 966 |         } catch (err) {
 967 |             print('\n  failed to load tests in [bold]{./test}\n');
 968 |             ++failures;
 969 |             process.exit(1);
 970 |         }
 971 |     }
 972 |     runFiles(files);
 973 | }
974 |
978 |

Show the cursor when show is true, otherwise hide it.

979 | 980 |

981 | 982 |
  • param: Boolean show

983 |
985 |
function cursor(show) {
 986 |     if (boring) return;
 987 |     if (show) {
 988 |         sys.print('\x1b[?25h');
 989 |     } else {
 990 |         sys.print('\x1b[?25l');
 991 |     }
 992 | }
993 |
997 |

Run the given test files.

998 | 999 |

1000 | 1001 |
  • param: Array files

1002 |
1004 |
function runFiles(files) {
1005 |     files = files.filter(function(file) {
1006 |         return file.match(file_matcher);
1007 |     });
1008 |     suites = files.length;
1009 | 
1010 |     if (serial) {
1011 |         (function next() {
1012 |             if (files.length) {
1013 |                 runFile(files.shift(), next);
1014 |             }
1015 |         })();
1016 |     } else {
1017 |         files.forEach(runFile);
1018 |     }
1019 | }
1020 |
1024 |

Run tests for the given file, callback fn() when finished.

1025 | 1026 |

1027 | 1028 |
  • param: String file

  • param: Function fn

1029 |
1031 |
function runFile(file, fn) {
1032 |     var title = path.basename(file),
1033 |         file = path.join(cwd, file),
1034 |         mod = require(file.replace(file_matcher,''));
1035 |     (function check() {
1036 |        var len = Object.keys(mod).length;
1037 |        if (len) {
1038 |            runSuite(title, mod, fn);
1039 |            suitesRun++;
1040 |        } else {
1041 |            setTimeout(check, 20);
1042 |        }
1043 |     })();
1044 | }
1045 |
1049 |

Run the given tests, callback fn() when finished.

1050 | 1051 |

1052 | 1053 |
  • param: String title

  • param: Object tests

  • param: Function fn

1054 |
1056 |
var dots = 0;
1057 | function runSuite(title, tests, callback) {
1058 |     // Keys
1059 |     var keys = only.length
1060 |         ? only.slice(0)
1061 |         : Object.keys(tests);
1062 | 
1063 |     // Setup
1064 |     var setup = tests.setup || function(fn, assert) { fn(); };
1065 |     var teardown = tests.teardown || function(fn, assert) { fn(); };
1066 | 
1067 |     process.setMaxListeners(10 + process.listeners('beforeExit').length  + keys.length);
1068 | 
1069 |     // Iterate tests
1070 |     (function next() {
1071 |         if (keys.length) {
1072 |             var key,
1073 |                 fn = tests[key = keys.shift()];
1074 |             // Non-tests
1075 |             if (key === 'setup' || key === 'teardown') return next();
1076 | 
1077 |             // Run test
1078 |             if (fn) {
1079 |                 var test = new Test({
1080 |                     fn: fn,
1081 |                     suite: title,
1082 |                     title: key,
1083 |                     setup: setup,
1084 |                     teardown: teardown
1085 |                 })
1086 |                 test.run(next);
1087 |             } else {
1088 |                 // @TODO: Add warning message that there's no test.
1089 |                 next();
1090 |             }
1091 |         } else if (serial) {
1092 |             callback();
1093 |         }
1094 |     })();
1095 | }
1096 | 
1097 | require('util').inherits(Test, require('events').EventEmitter);
1098 | function Test(options) {
1099 |     for (var key in options) {
1100 |         this[key] = options[key];
1101 |     }
1102 |     this._succeeded = [];
1103 |     this._failed = [];
1104 |     this._pending = [];
1105 |     this._beforeExit = [];
1106 |     this.assert = { __proto__: assert, _test: this };
1107 | 
1108 |     var test = this;
1109 |     process.on('beforeExit', function() {
1110 |         try {
1111 |             test.emit('exit');
1112 |         } catch (err) {
1113 |             test.failure(err);
1114 |         }
1115 |         test.report();
1116 |     });
1117 | }
1118 | 
1119 | Test.prototype.success = function(err) {
1120 |     this._succeeded.push(err);
1121 | };
1122 | 
1123 | Test.prototype.failure = function(err) {
1124 |     this._failed.push(err);
1125 |     this.error(err);
1126 | };
1127 | 
1128 | Test.prototype.report = function() {
1129 |     for (var i = 0; i &lt; this._pending.length; i++) {
1130 |         this.error(this._pending[i]);
1131 |     }
1132 | };
1133 | 
1134 | Test.prototype.run = function(callback) {
1135 |     try {
1136 |         ++testcount;
1137 |         assert._test = this;
1138 | 
1139 |         if (serial) {
1140 |             this.runSerial(callback);
1141 |         } else {
1142 |             // @TODO: find a way to run setup/tearDown.
1143 |             this.runParallel();
1144 |             callback();
1145 |         }
1146 |     } catch (err) {
1147 |         this.failure(err);
1148 |         this.report();
1149 |     }
1150 | };
1151 | 
1152 | Test.prototype.runSerial = function(callback) {
1153 |     var test = this;
1154 |     sys.print('.');
1155 |     if (++dots % 25 === 0) sys.print('\n');
1156 |     test.setup(function() {
1157 |         if (test.fn.length &lt; 1) {
1158 |             test.fn();
1159 |             test.teardown(callback);
1160 |         } else {
1161 |             var id = setTimeout(function() {
1162 |                 throw new Error(&quot;'" + key + "' timed out&quot;);
1163 |             }, timeout);
1164 |             test.callback = function() {
1165 |                 clearTimeout(id);
1166 |                 test.teardown(callback);
1167 |                 process.nextTick(function() {
1168 |                     test.report();
1169 |                 });
1170 |             };
1171 |             test.fn(test.callback);
1172 |         }
1173 |     });
1174 | };
1175 | 
1176 | Test.prototype.runParallel = function() {
1177 |     var test = this;
1178 |     test.fn(function(fn) {
1179 |         test.on('exit', function() {
1180 |             fn(test.assert);
1181 |         });
1182 |     }, test.assert);
1183 | };
1184 |
1188 |

Report err for the given test and suite.

1189 | 1190 |

1191 | 1192 |
  • param: String suite

  • param: String test

  • param: Error err

1193 |
1195 |
Test.prototype.error = function(err) {
1196 |     if (!err._reported) {
1197 |         ++failures;
1198 |         var name = err.name,
1199 |             stack = err.stack ? err.stack.replace(err.name, '') : '',
1200 |             label = this.title === 'uncaught'
1201 |                 ? this.title
1202 |                 : this.suite + ' ' + this.title;
1203 |         print('\n   [bold]{' + label + '}: [red]{' + name + '}' + stack + '\n');
1204 |         err._reported = true;
1205 |     }
1206 | }
1207 |
1211 |

Report exceptions. 1212 |

1213 |
1215 |
function report() {
1216 |     cursor(true);
1217 |     process.emit('beforeExit');
1218 |     if (suitesRun &lt; suites) {
1219 |         print('\n   [bold]{Failure}: [red]{Only ' + suitesRun + ' of ' + suites + ' suites have been started}\n\n');
1220 |     }
1221 |     else if (failures) {
1222 |         print('\n   [bold]{Failures}: [red]{' + failures + '}\n\n');
1223 |         notify('Failures: ' + failures);
1224 |     } else {
1225 |         if (serial) print('');
1226 |         print('\n   [green]{100%} ' + testcount + ' tests\n');
1227 |         notify('100% ok');
1228 |     }
1229 |     if (typeof _$jscoverage === 'object') {
1230 |         populateCoverage(_$jscoverage);
1231 |         if (!hasFullCoverage(_$jscoverage) || !quiet) {
1232 |             (jsonCoverage ? reportCoverageJson(_$jscoverage) : reportCoverageTable(_$jscoverage));
1233 |         }
1234 |     }
1235 | }
1236 |
1240 |

Growl notify the given msg.

1241 | 1242 |

1243 | 1244 |
  • param: String msg

1245 |
1247 |
function notify(msg) {
1248 |     if (growl) {
1249 |         childProcess.exec('growlnotify -name Expresso -m "' + msg + '"');
1250 |     }
1251 | }
1252 | 
1253 | // Report uncaught exceptions
1254 | var unknownTest = new Test({
1255 |     suite: 'uncaught',
1256 |     test: 'uncaught'
1257 | });
1258 | 
1259 | process.on('uncaughtException', function(err) {
1260 |     unknownTest.error(err);
1261 | });
1262 | 
1263 | // Show cursor
1264 | 
1265 | ['INT', 'TERM', 'QUIT'].forEach(function(sig) {
1266 |     process.on('SIG' + sig, function() {
1267 |         cursor(true);
1268 |         process.exit(1);
1269 |     });
1270 | });
1271 | 
1272 | // Report test coverage when available
1273 | // and emit "beforeExit" event to perform
1274 | // final assertions
1275 | 
1276 | var orig = process.emit;
1277 | process.emit = function(event) {
1278 |     if (event === 'exit') {
1279 |         report();
1280 |         process.reallyExit(failures);
1281 |     }
1282 |     orig.apply(this, arguments);
1283 | };
1284 | 
1285 | // Run test files
1286 | 
1287 | if (!defer) run(files);
1288 | 
1289 |
--------------------------------------------------------------------------------