diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..4d14aa3 --- /dev/null +++ b/Readme.md @@ -0,0 +1,56 @@ +# utest + +The minimal unit testing library. + +## Usage + +Running a test with utest is very simple: + +```js +var test = require('utest'); +var assert = require('assert'); + +test('Number#toFixed', { + 'returns a string': function() { + assert.equal(typeof (5).toFixed(), 'string'); + }, + + 'takes number of decimal places': function() { + assert.equal((5).toFixed(1), '5.0'); + }, + + 'does not round': function() { + assert.equal((5.55).toFixed(1), '5.5'); + }, +}); +``` + +The only additional feature is running a before/after method: + +```js +var test = require('utest'); +var assert = require('assert'); + +test('Date', { + before: function() { + this.date = new Date; + }, + + after: function() { + this.date = null; + }, + + 'lets you manipulate the year': function() { + this.date.setYear(2012); + assert.equal(this.date.getFullYear(), 2012); + }, + + 'can be coerced into a number': function() { + assert.equal(+this.date, 'number'); + }, +}); +``` + +## License + +This module is licensed under the MIT license. diff --git a/index.js b/index.js new file mode 100644 index 0000000..ec222e1 --- /dev/null +++ b/index.js @@ -0,0 +1,16 @@ +module.exports = utest; +utest.Collection = require('./lib/Collection'); +utest.TestCase = require('./lib/TestCase'); +utest.BashReporter = require('./lib/reporter/BashReporter'); + +var collection; +var reporter; +function utest(name, tests) { + if (!collection) { + collection = new utest.Collection(); + reporter = new utest.BashReporter({collection: collection}); + } + + var testCase = new utest.TestCase({name: name, tests: tests}); + collection.add(testCase); +}; diff --git a/lib/Collection.js b/lib/Collection.js new file mode 100644 index 0000000..db9be5c --- /dev/null +++ b/lib/Collection.js @@ -0,0 +1,33 @@ +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + +module.exports = Collection; +util.inherits(Collection, EventEmitter); +function Collection(options) { + this._pass = 0; + this._fail = 0; +} + +Collection.prototype.add = function(testCase) { + this._start = this._start || Date.now(); + + var self = this; + testCase + .on('pass', function(name) { + self._pass++; + self.emit('pass', testCase, name); + }) + .on('fail', function(name, err) { + self._fail++; + self.emit('fail', testCase, name, err); + }) + .run(); +}; + +Collection.prototype.stats = function() { + return { + pass : this._pass, + fail : this._fail, + duration : Date.now() - this._start, + }; +}; diff --git a/lib/TestCase.js b/lib/TestCase.js new file mode 100644 index 0000000..9e2a017 --- /dev/null +++ b/lib/TestCase.js @@ -0,0 +1,36 @@ +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + +module.exports = TestCase; +util.inherits(TestCase, EventEmitter); +function TestCase(properties) { + this.name = properties.name; + this._tests = properties.tests; +} + +TestCase.prototype.run = function() { + var noop = function() {}; + var before = this._tests.before || noop; + var after = this._tests.after || noop; + + for (var test in this._tests) { + if (test === 'before' || test === 'after') continue; + + var fn = this._tests[test]; + var context = {}; + + try { + before.call(context); + fn.call(context); + after.call(context); + } catch (_err) { + var err = _err; + } + + if (!err) { + this.emit('pass', test); + } else { + this.emit('fail', test, err); + } + } +}; diff --git a/lib/reporter/BashReporter.js b/lib/reporter/BashReporter.js new file mode 100644 index 0000000..d95b3f6 --- /dev/null +++ b/lib/reporter/BashReporter.js @@ -0,0 +1,26 @@ +module.exports = BashReporter; +function BashReporter(options) { + this._process = options.process || process; + this._collection = options.collection; + + this._collection.on('fail', this.handleFail.bind(this)); + this._process.on('exit', this.handleExit.bind(this)); +} + +BashReporter.prototype.handleFail = function(testCase, test, error) { + this._process.stdout.write('Fail: ' + testCase.name + ': ' + test + '\n'); + this._process.stdout.write(error.stack + '\n\n'); + +}; + +BashReporter.prototype.handleExit = function() { + var stats = this._collection.stats(); + this._process.stdout.write( + stats.fail + ' fail | ' + + stats.pass + ' pass | ' + + stats.duration + ' ms ' + + '\n' + ); + + if (stats.fail) this._process.reallyExit(1); +}; diff --git a/test/system/test-example.js b/test/system/test-example.js new file mode 100644 index 0000000..2f466d8 --- /dev/null +++ b/test/system/test-example.js @@ -0,0 +1,24 @@ +var assert = require('assert'); +var test = require('../..'); + +var executed = 0; +test('Number#toFixed', { + 'returns a string': function() { + assert.equal(typeof (5).toFixed(), 'string'); + executed++; + }, + + 'takes number of decimal places': function() { + assert.equal((5).toFixed(1), '5.0'); + executed++; + }, + + 'does not round': function() { + assert.equal((5.55).toFixed(1), '5.5'); + executed++; + }, +}); + +process.on('exit', function() { + assert.equal(executed, 3); +}); diff --git a/test/unit/test-Collection.js b/test/unit/test-Collection.js new file mode 100644 index 0000000..c25e77c --- /dev/null +++ b/test/unit/test-Collection.js @@ -0,0 +1,66 @@ +var assert = require('assert'); +var Collection = require('../../lib/Collection'); +var TestCase = require('../../lib/TestCase'); + +(function testRunningTwoTestCases() { + var caseA = new TestCase({ + name: 'a', + tests: { + '1': function() {}, + '2': function() {}, + }, + }); + var caseB = new TestCase({ + name: 'b', + tests: { + '3': function() {}, + '4': function() {}, + }, + }); + + var collection = new Collection(); + + var pass = []; + collection.on('pass', function(testCase, name) { + pass.push(testCase.name + ':' + name); + }); + + collection.add(caseA); + collection.add(caseB); + + assert.deepEqual(pass, ['a:1', 'a:2', 'b:3', 'b:4']); + + var stats = collection.stats(); + assert.equal(stats.pass, 4); +})(); + +(function testFailingTestCase() { + var testCase = new TestCase({ + tests: { + 'good test': function() { + }, + 'bad test': function() { + throw new Error('failure'); + } + }, + }); + + var collection = new Collection(); + + var fail = []; + collection.on('fail', function(testCase, name, error) { + fail.push({testCase: testCase, name: name, error: error}); + }); + + collection.add(testCase); + + assert.equal(fail.length, 1); + assert.equal(fail[0].testCase, testCase); + assert.equal(fail[0].name, 'bad test'); + assert.equal(fail[0].error.message, 'failure'); + + var stats = collection.stats(); + assert.equal(stats.pass, 1); + assert.equal(stats.fail, 1); + assert.ok(stats.duration >= 0); +})(); diff --git a/test/unit/test-TestCase.js b/test/unit/test-TestCase.js new file mode 100644 index 0000000..741d6f8 --- /dev/null +++ b/test/unit/test-TestCase.js @@ -0,0 +1,71 @@ +var assert = require('assert'); +var TestCase = require('../../lib/TestCase'); + +(function testRunEmitsPassAndFail() { + var test = new TestCase({tests: { + a: function() { + assert.equal(1, 1); + }, + b: function() { + assert.equal(2, 1); + }, + }}); + + var fail = []; + var pass = []; + + test + .on('pass', function(name) { + assert.equal(name, 'a'); + pass.push(name); + }) + .on('fail', function(name, err) { + assert.ok(err instanceof Error); + assert.equal(name, 'b'); + fail.push(name); + }); + + + test.run(); + + assert.equal(pass.length, 1); + assert.equal(fail.length, 1); +})(); + +(function testBeforeAndAfter() { + var events = []; + var test = new TestCase({tests: { + before: function() { + events.push('before'); + }, + after: function() { + events.push('after'); + }, + + a: function() { + events.push('a'); + }, + }}); + + test.run(); + + assert.deepEqual(events, ['before', 'a', 'after']); +})(); + +(function testBeforeAndAfterContext() { + var aCalled = false; + var test = new TestCase({tests: { + before: function() { + this.foo = 'bar'; + }, + + a: function() { + assert.deepEqual(this, {foo: 'bar'}); + aCalled = true; + }, + }}); + + test.run(); + + assert.ok(aCalled); +})();