diff --git a/lib/runner.js b/lib/runner.js index b8f5ee3ae..3b28219dc 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -16,7 +16,8 @@ var chainableMethods = { exclusive: false, skipped: false, todo: false, - callback: false + callback: false, + always: false }, chainableMethods: { test: {}, @@ -28,7 +29,8 @@ var chainableMethods = { only: {exclusive: true}, beforeEach: {type: 'beforeEach'}, afterEach: {type: 'afterEach'}, - cb: {callback: true} + cb: {callback: true}, + always: {always: true} } }; diff --git a/lib/test-collection.js b/lib/test-collection.js index 660eaf2d0..763ac4b0a 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -25,7 +25,9 @@ function TestCollection() { before: [], beforeEach: [], after: [], - afterEach: [] + afterAlways: [], + afterEach: [], + afterEachAlways: [] }; this._emitTestResult = this._emitTestResult.bind(this); @@ -58,13 +60,17 @@ TestCollection.prototype.add = function (test) { } } + if (metadata.always && type !== 'after' && type !== 'afterEach') { + throw new Error('"always" can only be used with after and afterEach hooks'); + } + // add a hook if (type !== 'test') { if (metadata.exclusive) { - throw new Error('"only" cannot be used with a ' + type + ' test'); + throw new Error('"only" cannot be used with a ' + type + ' hook'); } - this.hooks[type].push(test); + this.hooks[type + (metadata.always ? 'Always' : '')].push(test); return; } @@ -151,7 +157,7 @@ TestCollection.prototype._buildTest = function (test, context) { TestCollection.prototype._buildTestWithHooks = function (test) { if (test.metadata.skipped) { - return [this._skippedTest(this._buildTest(test))]; + return new Sequence([this._skippedTest(this._buildTest(test))], true); } var context = {context: {}}; @@ -159,12 +165,17 @@ TestCollection.prototype._buildTestWithHooks = function (test) { var beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, context); var afterHooks = this._buildHooks(this.hooks.afterEach, test.title, context); - return [].concat(beforeHooks, this._buildTest(test, context), afterHooks); + var sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, context), afterHooks), true); + if (this.hooks.afterEachAlways.length !== 0) { + var afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, context)); + sequence = new Sequence([sequence, afterAlwaysHooks], false); + } + return sequence; }; TestCollection.prototype._buildTests = function (tests) { return tests.map(function (test) { - return new Sequence(this._buildTestWithHooks(test), true); + return this._buildTestWithHooks(test); }, this); }; @@ -176,5 +187,10 @@ TestCollection.prototype.build = function (bail) { var concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), bail); var allTests = new Sequence([serialTests, concurrentTests]); - return new Sequence([beforeHooks, allTests, afterHooks], true); + var finalTests = new Sequence([beforeHooks, allTests, afterHooks], true); + if (this.hooks.afterAlways.length !== 0) { + var afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways)); + finalTests = new Sequence([finalTests, afterAlwaysHooks], false); + } + return finalTests; }; diff --git a/readme.md b/readme.md index 2a7af317d..750b13f28 100644 --- a/readme.md +++ b/readme.md @@ -418,9 +418,9 @@ test.todo('will think about writing this later'); AVA lets you register hooks that are run before and after your tests. This allows you to run setup and/or teardown code. -`test.before()` registers a hook to be run before the first test in your test file. Similarly `test.after()` registers a hook to be run after the last test. +`test.before()` registers a hook to be run before the first test in your test file. Similarly `test.after()` registers a hook to be run after the last test. Use `test.after.always()` to register a hook that will **always** run once your tests and other hooks complete. `.always()` hooks run regardless of whether there were earlier failures, so they are ideal for cleanup tasks. -`test.beforeEach()` registers a hook to be run before each test in your test file. Similarly `test.afterEach()` a hook to be run after each test. +`test.beforeEach()` registers a hook to be run before each test in your test file. Similarly `test.afterEach()` a hook to be run after each test. Use `test.afterEach.always()` to register an after hook that is called even if other test hooks, or the test itself, fail. `.always()` hooks are ideal for cleanup tasks. Like `test()` these methods take an optional title and a callback function. The title is shown if your hook fails to execute. The callback is called with an [execution object](#t). @@ -439,6 +439,10 @@ test.after('cleanup', t => { // this runs after all tests }); +test.after.always('guaranteed cleanup', t => { + // this will always run, regardless of earlier failures +}); + test.beforeEach(t => { // this runs before each test }); @@ -447,6 +451,10 @@ test.afterEach(t => { // this runs after each test }); +test.afterEach.always(t => { + // this runs after each test and other test hooks, even if they failed +}); + test(t => { // regular test }); diff --git a/test/hooks.js b/test/hooks.js index 6c6c28392..a2cf57799 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -59,6 +59,68 @@ test('after', function (t) { }); }); +test('after not run if test failed', function (t) { + t.plan(3); + + var runner = new Runner(); + var arr = []; + + runner.after(function () { + arr.push('a'); + }); + + runner.test(function () { + throw new Error('something went wrong'); + }); + runner.run({}).then(function (stats) { + t.is(stats.passCount, 0); + t.is(stats.failCount, 1); + t.strictDeepEqual(arr, []); + t.end(); + }); +}); + +test('after.always run even if test failed', function (t) { + t.plan(3); + + var runner = new Runner(); + var arr = []; + + runner.after.always(function () { + arr.push('a'); + }); + + runner.test(function () { + throw new Error('something went wrong'); + }); + runner.run({}).then(function (stats) { + t.is(stats.passCount, 0); + t.is(stats.failCount, 1); + t.strictDeepEqual(arr, ['a']); + t.end(); + }); +}); + +test('after.always run even if before failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.before(function () { + throw new Error('something went wrong'); + }); + + runner.after.always(function () { + arr.push('a'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, ['a']); + t.end(); + }); +}); + test('stop if before hooks failed', function (t) { t.plan(1); @@ -223,6 +285,110 @@ test('after each with serial tests', function (t) { }); }); +test('afterEach not run if concurrent tests failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.afterEach(function () { + arr.push('a'); + }); + + runner.test(function () { + throw new Error('something went wrong'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, []); + t.end(); + }); +}); + +test('afterEach not run if serial tests failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.afterEach(function () { + arr.push('a'); + }); + + runner.serial(function () { + throw new Error('something went wrong'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, []); + t.end(); + }); +}); + +test('afterEach.always run even if concurrent tests failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.afterEach.always(function () { + arr.push('a'); + }); + + runner.test(function () { + throw new Error('something went wrong'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, ['a']); + t.end(); + }); +}); + +test('afterEach.always run even if serial tests failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.afterEach.always(function () { + arr.push('a'); + }); + + runner.serial(function () { + throw new Error('something went wrong'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, ['a']); + t.end(); + }); +}); + +test('afterEach.always run even if beforeEach failed', function (t) { + t.plan(1); + + var runner = new Runner(); + var arr = []; + + runner.beforeEach(function () { + throw new Error('something went wrong'); + }); + + runner.test(function () { + arr.push('a'); + }); + + runner.afterEach.always(function () { + arr.push('b'); + }); + + runner.run({}).then(function () { + t.strictDeepEqual(arr, ['b']); + t.end(); + }); +}); + test('ensure hooks run only around tests', function (t) { t.plan(1); diff --git a/test/test-collection.js b/test/test-collection.js index f9630136d..c28932b29 100644 --- a/test/test-collection.js +++ b/test/test-collection.js @@ -8,7 +8,8 @@ function defaults() { serial: false, exclusive: false, skipped: false, - callback: false + callback: false, + always: false }; } @@ -69,7 +70,9 @@ function serialize(collection) { before: titles(collection.hooks.before), beforeEach: titles(collection.hooks.beforeEach), after: titles(collection.hooks.after), - afterEach: titles(collection.hooks.afterEach) + afterAlways: titles(collection.hooks.afterAlways), + afterEach: titles(collection.hooks.afterEach), + afterEachAlways: titles(collection.hooks.afterEachAlways) } }; @@ -96,7 +99,23 @@ test('throws if you try to set a hook as exclusive', function (t) { var collection = new TestCollection(); t.throws(function () { collection.add(mockTest({type: 'beforeEach', exclusive: true})); - }, {message: '"only" cannot be used with a beforeEach test'}); + }, {message: '"only" cannot be used with a beforeEach hook'}); + t.end(); +}); + +test('throws if you try to set a before hook as always', function (t) { + var collection = new TestCollection(); + t.throws(function () { + collection.add(mockTest({type: 'before', always: true})); + }, {message: '"always" can only be used with after and afterEach hooks'}); + t.end(); +}); + +test('throws if you try to set a test as always', function (t) { + var collection = new TestCollection(); + t.throws(function () { + collection.add(mockTest({always: true})); + }, {message: '"always" can only be used with after and afterEach hooks'}); t.end(); }); @@ -163,6 +182,17 @@ test('adding a after test', function (t) { t.end(); }); +test('adding a after.always test', function (t) { + var collection = new TestCollection(); + collection.add(mockTest({type: 'after', always: true}, 'bar')); + t.strictDeepEqual(serialize(collection), { + hooks: { + afterAlways: ['bar'] + } + }); + t.end(); +}); + test('adding a afterEach test', function (t) { var collection = new TestCollection(); collection.add(mockTest({type: 'afterEach'}, 'baz')); @@ -174,6 +204,17 @@ test('adding a afterEach test', function (t) { t.end(); }); +test('adding a afterEach.always test', function (t) { + var collection = new TestCollection(); + collection.add(mockTest({type: 'afterEach', always: true}, 'baz')); + t.strictDeepEqual(serialize(collection), { + hooks: { + afterEachAlways: ['baz'] + } + }); + t.end(); +}); + test('adding a bunch of different types', function (t) { var collection = new TestCollection(); collection.add(mockTest({}, 'a')); @@ -211,10 +252,12 @@ test('foo', function (t) { } add('after1', {type: 'after'}); + add('after.always', {type: 'after', always: true}); add('beforeEach1', {type: 'beforeEach'}); add('before1', {type: 'before'}); add('beforeEach2', {type: 'beforeEach'}); add('afterEach1', {type: 'afterEach'}); + add('afterEach.always', {type: 'afterEach', always: true}); add('test1', {}); add('afterEach2', {type: 'afterEach'}); add('test2', {}); @@ -233,13 +276,16 @@ test('foo', function (t) { 'test1', 'afterEach1 for test1', 'afterEach2 for test1', + 'afterEach.always for test1', 'beforeEach1 for test2', 'beforeEach2 for test2', 'test2', 'afterEach1 for test2', 'afterEach2 for test2', + 'afterEach.always for test2', 'after1', - 'after2' + 'after2', + 'after.always' ]); t.end(); @@ -266,10 +312,12 @@ test('foo', function (t) { } add('after1', {type: 'after'}); + add('after.always', {type: 'after', always: true}); add('beforeEach1', {type: 'beforeEach'}); add('before1', {type: 'before'}); add('beforeEach2', {type: 'beforeEach'}); add('afterEach1', {type: 'afterEach'}); + add('afterEach.always', {type: 'afterEach', always: true}); add('test1', {}); add('afterEach2', {type: 'afterEach'}); add('test2', {}); @@ -290,13 +338,16 @@ test('foo', function (t) { 'test1', 'afterEach1 for test1', 'afterEach2 for test1', + 'afterEach.always for test1', 'beforeEach1 for test2', 'beforeEach2 for test2', 'test2', 'afterEach1 for test2', 'afterEach2 for test2', + 'afterEach.always for test2', 'after1', - 'after2' + 'after2', + 'after.always' ]); t.end();