Skip to content

Commit 37acdb2

Browse files
claudeakshah123
authored andcommitted
feat: add --fail-hook-affected-tests option to report skipped tests as failed
When `before()` or `beforeEach()` hooks fail, Mocha currently only reports the hook failure and silently skips affected tests. This makes it difficult to track test coverage and understand the full impact of hook failures. This change introduces a new CLI option `--fail-hook-affected-tests` that, when enabled, reports all tests affected by hook failures as failed instead of silently skipping them. Changes: - Added --fail-hook-affected-tests boolean CLI option - Implemented failAffectedTests() method in Runner to fail all affected tests - Updated hook() method to call failAffectedTests when hooks fail - Added failHookAffectedTests option to Mocha class - Added integration tests for both before() and beforeEach() hook failures - All affected tests now receive descriptive error messages indicating which hook caused them to be skipped Fixes #4392
1 parent 9a70533 commit 37acdb2

File tree

7 files changed

+195
-1
lines changed

7 files changed

+195
-1
lines changed

lib/cli/run-option-metadata.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const TYPES = (exports.types = {
3535
"diff",
3636
"dry-run",
3737
"exit",
38+
"fail-hook-affected-tests",
3839
"pass-on-failing-test-suite",
3940
"fail-zero",
4041
"forbid-only",

lib/cli/run.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ exports.builder = (yargs) =>
103103
description: "Not fail test run if tests were failed",
104104
group: GROUPS.RULES,
105105
},
106+
"fail-hook-affected-tests": {
107+
description:
108+
"Report tests as failed when affected by hook failures (before/beforeEach)",
109+
group: GROUPS.RULES,
110+
},
106111
"fail-zero": {
107112
description: "Fail test run if no test(s) encountered",
108113
group: GROUPS.RULES,

lib/mocha.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,11 @@ Mocha.prototype.dryRun = function (dryRun) {
847847
* @return {Mocha} this
848848
* @chainable
849849
*/
850+
Mocha.prototype.failHookAffectedTests = function (failHookAffectedTests) {
851+
this.options.failHookAffectedTests = failHookAffectedTests !== false;
852+
return this;
853+
};
854+
850855
Mocha.prototype.failZero = function (failZero) {
851856
this.options.failZero = failZero !== false;
852857
return this;
@@ -966,6 +971,7 @@ Mocha.prototype.run = function (fn) {
966971
cleanReferencesAfterRun: this._cleanReferencesAfterRun,
967972
delay: options.delay,
968973
dryRun: options.dryRun,
974+
failHookAffectedTests: options.failHookAffectedTests,
969975
failZero: options.failZero,
970976
});
971977
createStatsCollector(runner);

lib/runner.js

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class Runner extends EventEmitter {
182182
* @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready.
183183
* @param {boolean} [opts.dryRun] - Whether to report tests without running them.
184184
* @param {boolean} [opts.failZero] - Whether to fail test run if zero tests encountered.
185+
* @param {boolean} [opts.failHookAffectedTests] - Whether to fail all tests affected by hook failures.
185186
*/
186187
constructor(suite, opts = {}) {
187188
super();
@@ -441,6 +442,47 @@ Runner.prototype.checkGlobals = function (test) {
441442
}
442443
};
443444

445+
/**
446+
* Fail all tests that are affected by a hook failure.
447+
* This is used when the `failHookAffectedTests` option is enabled.
448+
*
449+
* @private
450+
* @param {Suite} suite - The suite containing the affected tests
451+
* @param {Error} hookError - The error from the failed hook
452+
* @param {string} hookTitle - The title of the failed hook
453+
*/
454+
Runner.prototype.failAffectedTests = function (suite, hookError, hookTitle) {
455+
if (!this._opts.failHookAffectedTests) {
456+
return;
457+
}
458+
459+
var self = this;
460+
var errorMessage =
461+
'Test skipped due to failure in "' + hookTitle + '": ' + hookError.message;
462+
var testError = new Error(errorMessage);
463+
testError.stack = hookError.stack;
464+
465+
// Recursively fail all tests in this suite and its child suites
466+
function failTestsInSuite(s) {
467+
s.tests.forEach(function (test) {
468+
// Only fail tests that haven't been executed yet
469+
if (!test.state) {
470+
test.state = STATE_FAILED;
471+
self.failures++;
472+
self.emit(constants.EVENT_TEST_BEGIN, test);
473+
self.emit(constants.EVENT_TEST_FAIL, test, testError);
474+
self.emit(constants.EVENT_TEST_END, test);
475+
}
476+
});
477+
478+
s.suites.forEach(function (childSuite) {
479+
failTestsInSuite(childSuite);
480+
});
481+
}
482+
483+
failTestsInSuite(suite);
484+
};
485+
444486
/**
445487
* Fail the given `test`.
446488
*
@@ -583,6 +625,34 @@ Runner.prototype.hook = function (name, fn) {
583625
}
584626
} else if (err) {
585627
self.fail(hook, err);
628+
// If failHookAffectedTests is enabled, mark affected tests as failed
629+
if (self._opts.failHookAffectedTests) {
630+
if (name === HOOK_TYPE_BEFORE_ALL) {
631+
self.failAffectedTests(self.suite, err, hook.title);
632+
} else if (name === HOOK_TYPE_BEFORE_EACH) {
633+
// Fail the current test
634+
if (self.test && !self.test.state) {
635+
var errorMessage =
636+
'Test skipped due to failure in "' +
637+
hook.title +
638+
'": ' +
639+
err.message;
640+
var testError = new Error(errorMessage);
641+
testError.stack = err.stack;
642+
643+
self.test.state = STATE_FAILED;
644+
self.failures++;
645+
self.emit(constants.EVENT_TEST_BEGIN, self.test);
646+
self.emit(constants.EVENT_TEST_FAIL, self.test, testError);
647+
self.emit(constants.EVENT_TEST_END, self.test);
648+
}
649+
// Store the hook error info for remaining tests
650+
self._failedBeforeEachHook = {
651+
error: err,
652+
title: hook.title,
653+
};
654+
}
655+
}
586656
// stop executing hooks, notify callee of hook err
587657
return fn(err);
588658
}
@@ -734,10 +804,40 @@ Runner.prototype.runTests = function (suite, fn) {
734804
var tests = suite.tests.slice();
735805
var test;
736806

737-
function hookErr(_, errSuite, after) {
807+
function hookErr(err, errSuite, after) {
738808
// before/after Each hook for errSuite failed:
739809
var orig = self.suite;
740810

811+
// If failHookAffectedTests is enabled and this is a beforeEach failure,
812+
// mark remaining tests as failed
813+
if (
814+
self._opts.failHookAffectedTests &&
815+
!after &&
816+
self._failedBeforeEachHook
817+
) {
818+
// Fail all remaining tests in the suite
819+
var remainingTests = tests.slice();
820+
remainingTests.forEach(function (t) {
821+
if (!t.state) {
822+
var errorMessage =
823+
'Test skipped due to failure in "' +
824+
self._failedBeforeEachHook.title +
825+
'": ' +
826+
self._failedBeforeEachHook.error.message;
827+
var testError = new Error(errorMessage);
828+
testError.stack = self._failedBeforeEachHook.error.stack;
829+
830+
t.state = STATE_FAILED;
831+
self.failures++;
832+
self.emit(constants.EVENT_TEST_BEGIN, t);
833+
self.emit(constants.EVENT_TEST_FAIL, t, testError);
834+
self.emit(constants.EVENT_TEST_END, t);
835+
}
836+
});
837+
// Clear the stored hook info
838+
delete self._failedBeforeEachHook;
839+
}
840+
741841
// for failed 'after each' hook start from errSuite parent,
742842
// otherwise start from errSuite itself
743843
self.suite = after ? errSuite.parent : errSuite;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
describe('spec 1', function () {
4+
beforeEach(function () {
5+
throw new Error('before each hook error');
6+
});
7+
it('test 1', function () {
8+
// This should be reported as failed due to beforeEach hook failure
9+
});
10+
it('test 2', function () {
11+
// This should be reported as failed due to beforeEach hook failure
12+
});
13+
});
14+
describe('spec 2', function () {
15+
it('test 3', function () {
16+
// This should pass normally
17+
});
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
describe('spec 1', function () {
4+
before(function () {
5+
throw new Error('before hook error');
6+
});
7+
it('test 1', function () {
8+
// This should be reported as failed due to before hook failure
9+
});
10+
it('test 2', function () {
11+
// This should be reported as failed due to before hook failure
12+
});
13+
});
14+
describe('spec 2', function () {
15+
it('test 3', function () {
16+
// This should pass normally
17+
});
18+
});

test/integration/hook-err.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,52 @@ describe("hook error handling", function () {
267267
});
268268
});
269269

270+
describe("--fail-hook-affected-tests", function () {
271+
describe("before hook error", function () {
272+
it("should fail all affected tests", function (done) {
273+
runMochaJSON(
274+
"hooks/before-hook-error-with-fail-affected",
275+
["--fail-hook-affected-tests"],
276+
(err, res) => {
277+
if (err) {
278+
return done(err);
279+
}
280+
expect(res, "to have failed")
281+
.and("to have failed test count", 3)
282+
.and("to have failed test", '"before all" hook for "test 1"')
283+
.and("to have failed test", "test 1")
284+
.and("to have failed test", "test 2")
285+
.and("to have passed test count", 1)
286+
.and("to have passed test", "test 3");
287+
done();
288+
},
289+
);
290+
});
291+
});
292+
293+
describe("beforeEach hook error", function () {
294+
it("should fail all affected tests", function (done) {
295+
runMochaJSON(
296+
"hooks/before-each-hook-error-with-fail-affected",
297+
["--fail-hook-affected-tests"],
298+
(err, res) => {
299+
if (err) {
300+
return done(err);
301+
}
302+
expect(res, "to have failed")
303+
.and("to have failed test count", 3)
304+
.and("to have failed test", '"before each" hook for "test 1"')
305+
.and("to have failed test", "test 1")
306+
.and("to have failed test", "test 2")
307+
.and("to have passed test count", 1)
308+
.and("to have passed test", "test 3");
309+
done();
310+
},
311+
);
312+
});
313+
});
314+
});
315+
270316
function run(fnPath, outputFilter) {
271317
return (done) =>
272318
runMocha(fnPath, ["--reporter", "dot"], (err, res) => {

0 commit comments

Comments
 (0)