Skip to content
Merged
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
1 change: 1 addition & 0 deletions lib/cli/run-option-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const TYPES = (exports.types = {
"diff",
"dry-run",
"exit",
"fail-hook-affected-tests",
"pass-on-failing-test-suite",
"fail-zero",
"forbid-only",
Expand Down
5 changes: 5 additions & 0 deletions lib/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ exports.builder = (yargs) =>
description: "Not fail test run if tests were failed",
group: GROUPS.RULES,
},
"fail-hook-affected-tests": {
description:
"Report tests as failed when affected by hook failures (before/beforeEach)",
group: GROUPS.RULES,
},
"fail-zero": {
description: "Fail test run if no test(s) encountered",
group: GROUPS.RULES,
Expand Down
15 changes: 15 additions & 0 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,20 @@ Mocha.prototype.dryRun = function (dryRun) {
return this;
};

/**
* Reports tests as failed when they are skipped due to a hook failure.
*
* @public
* @see [CLI option](../#-fail-hook-affected-tests)
* @param {boolean} [failHookAffectedTests=true] - Whether to fail tests affected by hook failures.
* @return {Mocha} this
* @chainable
*/
Mocha.prototype.failHookAffectedTests = function (failHookAffectedTests) {
this.options.failHookAffectedTests = failHookAffectedTests !== false;
return this;
};

/**
* Fails test run if no tests encountered with exit-code 1.
*
Expand Down Expand Up @@ -966,6 +980,7 @@ Mocha.prototype.run = function (fn) {
cleanReferencesAfterRun: this._cleanReferencesAfterRun,
delay: options.delay,
dryRun: options.dryRun,
failHookAffectedTests: options.failHookAffectedTests,
failZero: options.failZero,
});
createStatsCollector(runner);
Expand Down
119 changes: 118 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class Runner extends EventEmitter {
* @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready.
* @param {boolean} [opts.dryRun] - Whether to report tests without running them.
* @param {boolean} [opts.failZero] - Whether to fail test run if zero tests encountered.
* @param {boolean} [opts.failHookAffectedTests] - Whether to fail all tests affected by hook failures.
*/
constructor(suite, opts = {}) {
super();
Expand Down Expand Up @@ -441,6 +442,73 @@ Runner.prototype.checkGlobals = function (test) {
}
};

/**
* Create an error object for a test that was skipped due to a hook failure.
*
* @private
* @param {string} hookTitle - The title of the failed hook
* @param {*} hookError - The error from the failed hook (may not be an Error object)
* @returns {Error} The error object for the skipped test
*/
function createHookSkipError(hookTitle, hookError) {
// Handle falsy or undefined exceptions
if (!hookError) {
hookError = createInvalidExceptionError(
'Hook "' + hookTitle + '" failed with exception: ' + hookError,
hookError,
);
}
// Convert non-Error objects to Error
else if (!isError(hookError)) {
hookError = thrown2Error(hookError);
}

var errorMessage =
'Test skipped due to failure in hook "' +
hookTitle +
'": ' +
hookError.message;
var testError = new Error(errorMessage);
testError.stack = hookError.stack;
return testError;
}

/**
* Fail all tests that are affected by a hook failure.
* This is used when the `failHookAffectedTests` option is enabled.
*
* @private
* @param {Suite} suite - The suite containing the affected tests
* @param {Error} hookError - The error from the failed hook
* @param {string} hookTitle - The title of the failed hook
*/
Runner.prototype.failAffectedTests = function (suite, hookError, hookTitle) {
if (!this._opts.failHookAffectedTests) {
return;
}

var self = this;
var testError = createHookSkipError(hookTitle, hookError);

// Recursively fail all tests in this suite and its child suites
function failTestsInSuite(s) {
s.tests.forEach(function (test) {
// Only fail tests that haven't been executed yet
if (!test.state) {
test.state = STATE_FAILED;
self.failures++;
self.emit(constants.EVENT_TEST_BEGIN, test);
self.emit(constants.EVENT_TEST_FAIL, test, testError);
self.emit(constants.EVENT_TEST_END, test);
}
});

s.suites.forEach(failTestsInSuite);
}

failTestsInSuite(suite);
};

/**
* Fail the given `test`.
*
Expand Down Expand Up @@ -583,6 +651,28 @@ Runner.prototype.hook = function (name, fn) {
}
} else if (err) {
self.fail(hook, err);
// If failHookAffectedTests is enabled, mark affected tests as failed
if (self._opts.failHookAffectedTests) {
if (name === HOOK_TYPE_BEFORE_ALL) {
self.failAffectedTests(self.suite, err, hook.title);
} else if (name === HOOK_TYPE_BEFORE_EACH) {
// Fail the current test
if (self.test && !self.test.state) {
var testError = createHookSkipError(hook.title, err);

self.test.state = STATE_FAILED;
self.failures++;
self.emit(constants.EVENT_TEST_BEGIN, self.test);
self.emit(constants.EVENT_TEST_FAIL, self.test, testError);
self.emit(constants.EVENT_TEST_END, self.test);
}
// Store the hook error info for remaining tests
self._failedBeforeEachHook = {
error: err,
title: hook.title,
};
}
}
// stop executing hooks, notify callee of hook err
return fn(err);
}
Expand Down Expand Up @@ -734,10 +824,37 @@ Runner.prototype.runTests = function (suite, fn) {
var tests = suite.tests.slice();
var test;

function hookErr(_, errSuite, after) {
function hookErr(err, errSuite, after) {
// before/after Each hook for errSuite failed:
var orig = self.suite;

// If failHookAffectedTests is enabled and this is a beforeEach failure,
// mark remaining tests as failed
if (
self._opts.failHookAffectedTests &&
!after &&
self._failedBeforeEachHook
) {
// Fail all remaining tests in the suite
var remainingTests = tests.slice();
remainingTests.forEach(function (t) {
if (!t.state) {
var testError = createHookSkipError(
self._failedBeforeEachHook.title,
self._failedBeforeEachHook.error,
);

t.state = STATE_FAILED;
self.failures++;
self.emit(constants.EVENT_TEST_BEGIN, t);
self.emit(constants.EVENT_TEST_FAIL, t, testError);
self.emit(constants.EVENT_TEST_END, t);
}
});
// Clear the stored hook info
delete self._failedBeforeEachHook;
}

// for failed 'after each' hook start from errSuite parent,
// otherwise start from errSuite itself
self.suite = after ? errSuite.parent : errSuite;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

describe('fails `beforeEach` hook', function () {
beforeEach(function () {
throw new Error('error in `beforeEach` hook');
});
it('test 1', function () {
// This should be reported as failed due to beforeEach hook failure
});
it('test 2', function () {
// This should be reported as failed due to beforeEach hook failure
});
});
describe('passes normally', function () {
it('test 3', function () {
// This should pass normally
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

describe('throws non-Error in `beforeEach` hook', function () {
describe('throws null', function () {
beforeEach(function () {
throw null;
});
it('test 1', function () {
// Should be reported as failed due to beforeEach hook failure
});
});

describe('throws undefined', function () {
beforeEach(function () {
throw undefined;
});
it('test 2', function () {
// Should be reported as failed due to beforeEach hook failure
});
});

describe('throws string', function () {
beforeEach(function () {
throw 'string error';
});
it('test 3', function () {
// Should be reported as failed due to beforeEach hook failure
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

describe('fails `before` hook', function () {
before(function () {
throw new Error('error in `before` hook');
});
it('test 1', function () {
// This should be reported as failed due to before hook failure
});
it('test 2', function () {
// This should be reported as failed due to before hook failure
});
});
describe('passes normally', function () {
it('test 3', function () {
// This should pass normally
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

describe('throws non-Error in `before` hook', function () {
describe('throws null', function () {
before(function () {
throw null;
});
it('test 1', function () {
// Should be reported as failed due to before hook failure
});
});

describe('throws undefined', function () {
before(function () {
throw undefined;
});
it('test 2', function () {
// Should be reported as failed due to before hook failure
});
});

describe('throws string', function () {
before(function () {
throw 'string error';
});
it('test 3', function () {
// Should be reported as failed due to before hook failure
});
});

describe('throws number', function () {
before(function () {
throw 42;
});
it('test 4', function () {
// Should be reported as failed due to before hook failure
});
});
});
Loading
Loading