diff --git a/docs/index.md b/docs/index.md index 1a67af108a..2b1bbd64c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -840,6 +840,7 @@ Rules & Behavior --check-leaks Check for global variable leaks [boolean] --delay Delay initial execution of root suite [boolean] --exit Force Mocha to quit after tests complete [boolean] + --forbid-empty-suite Fail if a test suite contains no tests [boolean] --forbid-only Fail if exclusive test(s) encountered [boolean] --forbid-pending Fail if pending test(s) encountered [boolean] --global, --globals List of allowed global variables [array] @@ -954,6 +955,12 @@ To ensure your tests aren't leaving messes around, here are some ideas to get st - Try something like [wtfnode][npm-wtfnode] - Use [`.only`](#exclusive-tests) until you find the test that causes Mocha to hang +### `--forbid-empty-suite` + +Enforce a rule that test suites must define at least one test, either directly or in an inner suite. + +`--forbid-empty-suite` causes Mocha to fail when an empty suite is encountered. + ### `--forbid-only` Enforce a rule that tests may not be exclusive (use of e.g., `describe.only()` or `it.only()` is disallowed). diff --git a/lib/cli/run-option-metadata.js b/lib/cli/run-option-metadata.js index d0bc92ffbe..3b0041c4ca 100644 --- a/lib/cli/run-option-metadata.js +++ b/lib/cli/run-option-metadata.js @@ -33,6 +33,7 @@ exports.types = { 'delay', 'diff', 'exit', + 'forbid-empty-suite', 'forbid-only', 'forbid-pending', 'full-trace', diff --git a/lib/cli/run.js b/lib/cli/run.js index 014227d569..7316044707 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -106,6 +106,10 @@ exports.builder = yargs => normalize: true, requiresArg: true }, + 'forbid-empty-suite': { + description: 'Fail if a test suite contains no tests', + group: GROUPS.RULES + }, 'forbid-only': { description: 'Fail if exclusive test(s) encountered', group: GROUPS.RULES diff --git a/lib/mocha.js b/lib/mocha.js index 740e1fd841..a492357b8a 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -75,6 +75,7 @@ exports.Test = require('./test'); * @param {boolean} [options.delay] - Delay root suite execution? * @param {boolean} [options.diff] - Show diff on failure? * @param {string} [options.fgrep] - Test filter given string. + * @param {boolean} [options.forbidEmptySuite] - Require at least one test. * @param {boolean} [options.forbidOnly] - Tests marked `only` fail the suite? * @param {boolean} [options.forbidPending] - Pending tests fail the suite? * @param {boolean} [options.fullTrace] - Full stacktrace upon failure? @@ -125,6 +126,7 @@ function Mocha(options) { 'color', 'delay', 'diff', + 'forbidEmptySuite', 'forbidOnly', 'forbidPending', 'fullTrace', @@ -828,6 +830,21 @@ Mocha.prototype.delay = function delay() { return this; }; +/** + * Causes running a suite with no tests to fail it. + * + * @public + * @see [CLI option](../#-forbid-empty-suite) + * @param {boolean} [forbidEmptySuite=true] - Whether each suite is required to + * define at least one test. + * @returns {Mocha} this + * @chainable + */ +Mocha.prototype.forbidEmptySuite = function(forbidEmptySuite) { + this.options.forbidEmptySuite = forbidEmptySuite !== false; + return this; +}; + /** * Causes tests marked `only` to fail the suite. * @@ -909,6 +926,7 @@ Mocha.prototype.run = function(fn) { runner.fullStackTrace = options.fullTrace; runner.asyncOnly = options.asyncOnly; runner.allowUncaught = options.allowUncaught; + runner.forbidEmptySuite = options.forbidEmptySuite; runner.forbidOnly = options.forbidOnly; runner.forbidPending = options.forbidPending; if (options.grep) { diff --git a/lib/runner.js b/lib/runner.js index 8e7c8736c0..6d25448639 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -732,6 +732,9 @@ Runner.prototype.runSuite = function(suite, fn) { debug('run suite %s', suite.fullTitle()); + if (!total && this.forbidEmptySuite) { + this.fail(suite, new Error('Empty suite forbidden')); + } if (!total || (self.failures && suite._bail)) { return fn(); } diff --git a/test/integration/fixtures/options/forbid-empty-suite/dynamically-added-test.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/dynamically-added-test.fixture.js new file mode 100644 index 0000000000..fd9a2a2178 --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/dynamically-added-test.fixture.js @@ -0,0 +1,14 @@ +'use strict'; + +describe('suite with dynamically added test', function() { + const suite = this; + before(function() { + suite.suites[1].addTest(it('added test', function() {})); + }); + + describe('A', function() { + it('existing test', () => {}); + }); + + describe('B', function() {}); +}); diff --git a/test/integration/fixtures/options/forbid-empty-suite/empty-nested-suite.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/empty-nested-suite.fixture.js new file mode 100644 index 0000000000..5e4f17614a --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/empty-nested-suite.fixture.js @@ -0,0 +1,8 @@ +'use strict'; + +describe('parent suite', function() { + describe('suite with test', function() { + it('it nested', function() {}); + }); + describe('empty suite', function() {}); +}); diff --git a/test/integration/fixtures/options/forbid-empty-suite/empty-suite.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/empty-suite.fixture.js new file mode 100644 index 0000000000..c6a95699ed --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/empty-suite.fixture.js @@ -0,0 +1,3 @@ +'use strict'; + +describe('forbid empty suite - empty', function() {}); diff --git a/test/integration/fixtures/options/forbid-empty-suite/empty.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/empty.fixture.js new file mode 100644 index 0000000000..ad9a93a7c1 --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/empty.fixture.js @@ -0,0 +1 @@ +'use strict'; diff --git a/test/integration/fixtures/options/forbid-empty-suite/nested-suite.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/nested-suite.fixture.js new file mode 100644 index 0000000000..597e9d5907 --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/nested-suite.fixture.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('parent suite', function() { + describe('suite with test', function() { + it('it nested', function() {}); + }); +}); diff --git a/test/integration/fixtures/options/forbid-empty-suite/passed.fixture.js b/test/integration/fixtures/options/forbid-empty-suite/passed.fixture.js new file mode 100644 index 0000000000..23e3d597c4 --- /dev/null +++ b/test/integration/fixtures/options/forbid-empty-suite/passed.fixture.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('forbid empty suite - not empty', function() { + it('test1', function() {}); + it('test2', function() {}); + it('test3', function() {}); +}); diff --git a/test/integration/options/forbidEmptySuite.spec.js b/test/integration/options/forbidEmptySuite.spec.js new file mode 100644 index 0000000000..4c4287731a --- /dev/null +++ b/test/integration/options/forbidEmptySuite.spec.js @@ -0,0 +1,84 @@ +'use strict'; + +var path = require('path').posix; +var helpers = require('../helpers'); +var runMocha = helpers.runMocha; +var runMochaJSON = helpers.runMochaJSON; + +describe('--forbid-empty-suite', function() { + var args = []; + var emptySuiteErrorMessage = 'Empty suite forbidden'; + + before(function() { + args = ['--forbid-empty-suite']; + }); + + it('should succeed if there are tests', function(done) { + var fixture = path.join('options', 'forbid-empty-suite', 'passed'); + runMochaJSON(fixture, args, function(err, res) { + if (err) { + return done(err); + } + expect(res, 'to have passed'); + done(); + }); + }); + + it('should succeed if there is an inner suite with tests', function(done) { + var fixture = path.join('options', 'forbid-empty-suite', 'nested-suite'); + runMochaJSON(fixture, args, function(err, res) { + if (err) { + return done(err); + } + expect(res, 'to have passed'); + done(); + }); + }); + + it('should succeed if there are dynamically added tests', function(done) { + var fixture = path.join( + 'options', + 'forbid-empty-suite', + 'dynamically-added-test' + ); + runMochaJSON(fixture, args, function(err, res) { + if (err) { + return done(err); + } + expect(res, 'to have passed'); + done(); + }); + }); + + var forbidEmptySuiteFailureTests = { + 'should fail if there are no test suites': 'empty', + 'should fail if there are no tests': 'empty-suite', + 'should fail if there is an inner suite with no tests': 'empty-nested-suite' + }; + + Object.keys(forbidEmptySuiteFailureTests).forEach(function(title) { + it(title, function(done) { + var fixture = path.join( + 'options', + 'forbid-empty-suite', + forbidEmptySuiteFailureTests[title] + ); + var spawnOpts = {stdio: 'pipe'}; + runMocha( + fixture, + args, + function(err, res) { + if (err) { + return done(err); + } + expect(res, 'to satisfy', { + code: 1, + output: new RegExp(emptySuiteErrorMessage) + }); + done(); + }, + spawnOpts + ); + }); + }); +}); diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index bc460aa845..c2f0a77dec 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -191,6 +191,25 @@ describe('Mocha', function() { }); }); + describe('#forbidEmptySuite()', function() { + it('should set the forbidEmptySuite option to true', function() { + var mocha = new Mocha(opts); + mocha.forbidEmptySuite(); + expect(mocha.options, 'to have property', 'forbidEmptySuite', true); + }); + + it('should set the forbidEmptySuite option to false', function() { + var mocha = new Mocha(opts); + mocha.forbidEmptySuite(false); + expect(mocha.options, 'to have property', 'forbidEmptySuite', false); + }); + + it('should be chainable', function() { + var mocha = new Mocha(opts); + expect(mocha.forbidEmptySuite(), 'to be', mocha); + }); + }); + describe('#forbidOnly()', function() { it('should set the forbidOnly option to true', function() { var mocha = new Mocha(opts);