diff --git a/docs/index.md b/docs/index.md index b56610e043..bbca4974c2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -597,6 +597,20 @@ before(function() { > Before Mocha v3.0.0, `this.skip()` was not supported in asynchronous tests and hooks. +You may provide a `reason` argument to `this.skip()` which can be helpful in self documenting test results. + +```js +it('should only test in the correct environment', function() { + if (/* check test environment */) { + // make assertions + } else { + this.skip('the environment is not correct'); + } +}); +``` + +*Note*: The `reason` argument is only supported by `this.skip()`, not `.skip()`. If you want to mark a test as pending with a reason, it must be done at runtime. + ## Retry Tests You can choose to retry failed tests up to a certain number of times. This feature is designed to handle end-to-end tests (functional tests/Selenium...) where resources cannot be easily mocked/stubbed. **It's not recommended to use this feature for unit tests**. diff --git a/lib/context.js b/lib/context.js index 812162b137..950d9bc466 100644 --- a/lib/context.js +++ b/lib/context.js @@ -80,9 +80,10 @@ Context.prototype.slow = function(ms) { * * @api private * @throws Pending + * @param {string} reason */ -Context.prototype.skip = function() { - this.runnable().skip(); +Context.prototype.skip = function(reason) { + this.runnable().skip(reason); }; /** diff --git a/lib/reporters/json.js b/lib/reporters/json.js index 046e4ba4a5..ebd241daa9 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -73,18 +73,25 @@ function JSONReporter(runner) { * @return {Object} */ function clean(test) { + var res; var err = test.err || {}; if (err instanceof Error) { err = errorJSON(err); } - return { + res = { title: test.title, fullTitle: test.fullTitle(), duration: test.duration, currentRetry: test.currentRetry(), err: cleanCycles(err) }; + + if (test.reason) { + res.reason = test.reason; + } + + return res; } /** diff --git a/lib/reporters/spec.js b/lib/reporters/spec.js index 75e6dda1b5..6c9532fc8f 100644 --- a/lib/reporters/spec.js +++ b/lib/reporters/spec.js @@ -55,7 +55,10 @@ function Spec(runner) { runner.on('pending', function(test) { var fmt = indent() + color('pending', ' - %s'); - console.log(fmt, test.title); + console.log( + fmt, + test.title + (test.reason ? ' (' + test.reason + ')' : '') + ); }); runner.on('pass', function(test) { diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index c1a930d2d8..93d983706a 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -173,7 +173,13 @@ XUnit.prototype.test = function(test) { ) ); } else if (test.isPending()) { - this.write(tag('testcase', attrs, false, tag('skipped', {}, true))); + var skippedAttrs = {}; + if (test.reason) { + skippedAttrs.message = test.reason; + } + this.write( + tag('testcase', attrs, false, tag('skipped', skippedAttrs, true)) + ); } else { this.write(tag('testcase', attrs, true)); } diff --git a/lib/runnable.js b/lib/runnable.js index 73da817793..dcb4de84a0 100644 --- a/lib/runnable.js +++ b/lib/runnable.js @@ -116,9 +116,14 @@ Runnable.prototype.enableTimeouts = function(enabled) { * @memberof Mocha.Runnable * @public * @api public + * @param {string} reason - Reason the Runnable is being skipped. */ -Runnable.prototype.skip = function() { - throw new Pending('sync skip'); +Runnable.prototype.skip = function(reason) { + if (reason) { + reason = String(reason); + this.reason = reason; + } + throw new Pending(reason); }; /** @@ -327,8 +332,12 @@ Runnable.prototype.run = function(fn) { this.resetTimeout(); // allows skip() to be used in an explicit async context - this.skip = function asyncSkip() { - done(new Pending('async skip call')); + this.skip = function asyncSkip(reason) { + if (reason) { + reason = String(reason); + this.reason = reason; + } + done(new Pending(reason)); // halt execution. the Runnable will be marked pending // by the previous call, and the uncaught handler will ignore // the failure. diff --git a/lib/runner.js b/lib/runner.js index 6aefb34cce..5b2c1a17ec 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -323,9 +323,15 @@ Runner.prototype.hook = function(name, fn) { if (err instanceof Pending) { if (name === 'beforeEach' || name === 'afterEach') { self.test.pending = true; + if (err.message) { + self.test.reason = err.message; + } } else { suite.tests.forEach(function(test) { test.pending = true; + if (err.message) { + test.reason = err.message; + } }); // a pending hook won't be executed twice. hook.pending = true; diff --git a/test/integration/fixtures/pending/skip-async-before-with-reason.fixture.js b/test/integration/fixtures/pending/skip-async-before-with-reason.fixture.js new file mode 100644 index 0000000000..fe6e6b252c --- /dev/null +++ b/test/integration/fixtures/pending/skip-async-before-with-reason.fixture.js @@ -0,0 +1,18 @@ +'use strict'; + +describe('skip in before with reason', function () { + before(function (done) { + var self = this; + setTimeout(function () { + self.skip('skip reason'); + }, 50); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); +}); diff --git a/test/integration/fixtures/pending/skip-async-beforeEach-with-reason.fixture.js b/test/integration/fixtures/pending/skip-async-beforeEach-with-reason.fixture.js new file mode 100644 index 0000000000..e888b19e95 --- /dev/null +++ b/test/integration/fixtures/pending/skip-async-beforeEach-with-reason.fixture.js @@ -0,0 +1,18 @@ +'use strict'; + +describe('skip in beforeEach with reason', function () { + beforeEach(function (done) { + var self = this; + setTimeout(function () { + self.skip('skip reason'); + }, 50); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); +}); diff --git a/test/integration/fixtures/pending/skip-async-spec-with-reason.fixture.js b/test/integration/fixtures/pending/skip-async-spec-with-reason.fixture.js new file mode 100644 index 0000000000..0c964298ff --- /dev/null +++ b/test/integration/fixtures/pending/skip-async-spec-with-reason.fixture.js @@ -0,0 +1,14 @@ +'use strict'; + +describe('skip in test with reason', function () { + it('should skip async with reason', function (done) { + var self = this; + setTimeout(function () { + self.skip('skip reason'); + }, 50); + }); + + it('should run other tests in the suite', function () { + // Do nothing + }); +}); diff --git a/test/integration/fixtures/pending/skip-sync-before-with-reason.fixture.js b/test/integration/fixtures/pending/skip-sync-before-with-reason.fixture.js new file mode 100644 index 0000000000..968c4eafea --- /dev/null +++ b/test/integration/fixtures/pending/skip-sync-before-with-reason.fixture.js @@ -0,0 +1,15 @@ +'use strict'; + +describe('skip in before with reason', function () { + before(function () { + this.skip('skip reason'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); +}); diff --git a/test/integration/fixtures/pending/skip-sync-beforeEach-with-reason.fixture.js b/test/integration/fixtures/pending/skip-sync-beforeEach-with-reason.fixture.js new file mode 100644 index 0000000000..c7e494154f --- /dev/null +++ b/test/integration/fixtures/pending/skip-sync-beforeEach-with-reason.fixture.js @@ -0,0 +1,15 @@ +'use strict'; + +describe('skip in beforeEach with reason', function () { + beforeEach(function () { + this.skip('skip reason'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); + + it('should never run this test', function () { + throw new Error('never thrown'); + }); +}); diff --git a/test/integration/fixtures/pending/skip-sync-spec-with-reason.fixture.js b/test/integration/fixtures/pending/skip-sync-spec-with-reason.fixture.js new file mode 100644 index 0000000000..b6c272596d --- /dev/null +++ b/test/integration/fixtures/pending/skip-sync-spec-with-reason.fixture.js @@ -0,0 +1,12 @@ +'use strict'; + +describe('skip in test with reason', function () { + it('should skip immediately with reason', function () { + this.skip('skip reason'); + throw new Error('never thrown'); + }); + + it('should run other tests in the suite', function () { + // Do nothing + }); +}); diff --git a/test/integration/pending.spec.js b/test/integration/pending.spec.js index 27601b7a0e..4a160fb135 100644 --- a/test/integration/pending.spec.js +++ b/test/integration/pending.spec.js @@ -61,6 +61,25 @@ describe('pending', function() { assert.equal(res.stats.passes, 1); assert.equal(res.stats.failures, 0); assert.equal(res.code, 0); + assert.ok(!res.pending[0].hasOwnProperty('reason')); + done(); + }); + }); + + it('should allow a skip reason', function(done) { + run('pending/skip-sync-spec-with-reason.fixture.js', args, function( + err, + res + ) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 1); + assert.equal(res.stats.passes, 1); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); done(); }); }); @@ -80,6 +99,25 @@ describe('pending', function() { done(); }); }); + + it('should allow a skip reason', function(done) { + run('pending/skip-sync-before-with-reason.fixture.js', args, function( + err, + res + ) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 2); + assert.equal(res.stats.passes, 0); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); + assert.equal(res.pending[1].reason, 'skip reason'); + done(); + }); + }); }); describe('in beforeEach', function() { @@ -99,6 +137,26 @@ describe('pending', function() { done(); }); }); + + it('should allow a skip reason', function(done) { + run( + 'pending/skip-sync-beforeEach-with-reason.fixture.js', + args, + function(err, res) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 2); + assert.equal(res.stats.passes, 0); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); + assert.equal(res.pending[1].reason, 'skip reason'); + done(); + } + ); + }); }); }); @@ -114,6 +172,25 @@ describe('pending', function() { assert.equal(res.stats.passes, 1); assert.equal(res.stats.failures, 0); assert.equal(res.code, 0); + assert.ok(!res.pending[0].hasOwnProperty('reason')); + done(); + }); + }); + + it('should allow a skip reason', function(done) { + run('pending/skip-async-spec-with-reason.fixture.js', args, function( + err, + res + ) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 1); + assert.equal(res.stats.passes, 1); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); done(); }); }); @@ -130,6 +207,27 @@ describe('pending', function() { assert.equal(res.stats.passes, 0); assert.equal(res.stats.failures, 0); assert.equal(res.code, 0); + assert.ok(!res.pending[0].hasOwnProperty('reason')); + assert.ok(!res.pending[1].hasOwnProperty('reason')); + done(); + }); + }); + + it('should allow a skip reason', function(done) { + run('pending/skip-async-before-with-reason.fixture.js', args, function( + err, + res + ) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 2); + assert.equal(res.stats.passes, 0); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); + assert.equal(res.pending[1].reason, 'skip reason'); done(); }); }); @@ -149,9 +247,31 @@ describe('pending', function() { assert.equal(res.stats.passes, 0); assert.equal(res.stats.failures, 0); assert.equal(res.code, 0); + assert.ok(!res.pending[0].hasOwnProperty('reason')); + assert.ok(!res.pending[1].hasOwnProperty('reason')); done(); }); }); + + it('should allow a skip reason', function(done) { + run( + 'pending/skip-sync-beforeEach-with-reason.fixture.js', + args, + function(err, res) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 2); + assert.equal(res.stats.passes, 0); + assert.equal(res.stats.failures, 0); + assert.equal(res.code, 0); + assert.equal(res.pending[0].reason, 'skip reason'); + assert.equal(res.pending[1].reason, 'skip reason'); + done(); + } + ); + }); }); }); }); diff --git a/test/reporters/spec.spec.js b/test/reporters/spec.spec.js index 2f4eee33e5..954516b818 100644 --- a/test/reporters/spec.spec.js +++ b/test/reporters/spec.spec.js @@ -12,6 +12,7 @@ describe('Spec reporter', function() { var runner; var useColors; var expectedTitle = 'expectedTitle'; + var expectedReason = 'expected reason'; beforeEach(function() { stdout = []; @@ -52,6 +53,19 @@ describe('Spec reporter', function() { var expectedArray = [' - ' + expectedTitle + '\n']; expect(stdout, 'to equal', expectedArray); }); + it('should return title and reason', function() { + var suite = { + title: expectedTitle, + reason: expectedReason + }; + runner = createMockRunner('pending test', 'pending', null, null, suite); + Spec.call({epilogue: function() {}}, runner); + process.stdout.write = stdoutWrite; + var expectedArray = [ + ' - ' + expectedTitle + ' (' + expectedReason + ')' + '\n' + ]; + expect(stdout, 'to equal', expectedArray); + }); }); describe('on pass', function() { describe('if test speed is slow', function() { diff --git a/test/reporters/xunit.spec.js b/test/reporters/xunit.spec.js index 8108c50b78..dfd6b29e86 100644 --- a/test/reporters/xunit.spec.js +++ b/test/reporters/xunit.spec.js @@ -20,6 +20,7 @@ describe('XUnit reporter', function() { var expectedMessage = 'some message'; var expectedStack = 'some-stack'; var expectedWrite = null; + var expectedReason = 'some reason'; beforeEach(function() { stdout = []; @@ -275,6 +276,42 @@ describe('XUnit reporter', function() { expect(expectedWrite, 'to be', expectedTag); }); + it('should write expected tag with message', function() { + var xunit = new XUnit({on: function() {}, once: function() {}}); + + var expectedTest = { + isPending: function() { + return true; + }, + title: expectedTitle, + parent: { + fullTitle: function() { + return expectedClassName; + } + }, + duration: 1000, + reason: expectedReason + }; + xunit.test.call( + { + write: function(string) { + expectedWrite = string; + } + }, + expectedTest + ); + + var expectedTag = + ''; + + expect(expectedWrite, 'to be', expectedTag); + }); }); describe('on test in any other state', function() { it('should write expected tag', function() {