Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to forbid empty test suites #4123

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions lib/cli/run-option-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ exports.types = {
'delay',
'diff',
'exit',
'forbid-empty-suite',
'forbid-only',
'forbid-pending',
'full-trace',
Expand Down
4 changes: 4 additions & 0 deletions lib/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -125,6 +126,7 @@ function Mocha(options) {
'color',
'delay',
'diff',
'forbidEmptySuite',
'forbidOnly',
'forbidPending',
'fullTrace',
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suite does not extend Runnable (as does Test and Hook), so you can't fail a Suite.
The reporter output is weird, if the test file contains only an empty suite.
Maybe a dummy test?

}
if (!total || (self.failures && suite._bail)) {
return fn();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

describe('parent suite', function() {
describe('suite with test', function() {
it('it nested', function() {});
});
describe('empty suite', function() {});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

describe('forbid empty suite - empty', function() {});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'use strict';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

describe('parent suite', function() {
describe('suite with test', function() {
it('it nested', function() {});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

describe('forbid empty suite - not empty', function() {
it('test1', function() {});
it('test2', function() {});
it('test3', function() {});
});
84 changes: 84 additions & 0 deletions test/integration/options/forbidEmptySuite.spec.js
Original file line number Diff line number Diff line change
@@ -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
);
});
});
});
19 changes: 19 additions & 0 deletions test/unit/mocha.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down