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

fix: improve error handling in global fixtures #5208 #5275

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
31 changes: 25 additions & 6 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ function Mocha(options = {}) {
options = {...mocharc, ...options};
this.files = [];
this.options = options;

// root suite
this.suite = new exports.Suite('', new exports.Context(), true);
this._cleanReferencesAfterRun = true;
Expand Down Expand Up @@ -987,6 +988,7 @@ Mocha.prototype.run = function (fn) {
if (this.files.length && !this._lazyLoadFiles) {
this.loadFiles();
}

var suite = this.suite;
var options = this.options;
options.files = this.files;
Expand Down Expand Up @@ -1190,7 +1192,7 @@ Mocha.prototype.runGlobalSetup = async function runGlobalSetup(context = {}) {
const {globalSetup} = this.options;
if (globalSetup && globalSetup.length) {
debug('run(): global setup starting');
await this._runGlobalFixtures(globalSetup, context);
await this._runGlobalFixtures(globalSetup, context, "Global Setup");
debug('run(): global setup complete');
}
return context;
Expand All @@ -1212,29 +1214,45 @@ Mocha.prototype.runGlobalTeardown = async function runGlobalTeardown(
const {globalTeardown} = this.options;
if (globalTeardown && globalTeardown.length) {
debug('run(): global teardown starting');
await this._runGlobalFixtures(globalTeardown, context);
await this._runGlobalFixtures(globalTeardown, context, "Global Teardown");
}
debug('run(): global teardown complete');
return context;
};

/**
* Run global fixtures sequentially with context `context`
* Run global fixtures sequentially with context `context`.
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

@TG199 this piece of feedback is still open

* @private
* @param {MochaGlobalFixture[]} [fixtureFns] - Fixtures to run
* @param {object} [context] - context object
* @params {string} [phase] - phase of the fixture
* @returns {Promise<object>} context object
*/
Mocha.prototype._runGlobalFixtures = async function _runGlobalFixtures(
fixtureFns = [],
context = {}
context = {},
phase
) {
for await (const fixtureFn of fixtureFns) {
await fixtureFn.call(context);
for (const fixtureFn of fixtureFns) {
try {
await fixtureFn.call(context);
} catch (err) {

context.stats.failures++;
context.failures++;

context.emit('fail', {
title: phase,
err: err
});
console.error(err);
process.exit(1);
Copy link
Member

Choose a reason for hiding this comment

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

[Bug] We can't manually process.exit(1) ourselves. Someone might be calling the Mocha API manually and expect to get results returned back to them. If you want to mark all tests as failed, you'll have to find another way.

}
}
return context;
};


/**
* Toggle execution of any global setup fixture(s)
*
Expand Down Expand Up @@ -1330,3 +1348,4 @@ Mocha.prototype.hasGlobalTeardownFixtures =
* @param {Array<*>} impls - User-supplied implementations
* @returns {Promise<*>|*}
*/

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';


describe('Test Suite', function() {
it('failing test', function() {
throw new Error('Test failure');
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

exports.mochaGlobalSetup = async function () {
throw new Error('Setup problem');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

exports.mochaGlobalTeardown = async function () {
throw new Error('Teardown problem');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

exports.mochaGlobalSetup = async function () {
// Success case
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

exports.mochaGlobalTeardown = async function () {
// Success case
};

7 changes: 7 additions & 0 deletions test/integration/fixtures/global-fixtures/test.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

describe('Test Suite', function() {
it('should pass', function() {
// This test passes
});
});
9 changes: 9 additions & 0 deletions test/integration/fixtures/global-teardown-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const { it } = require('../../../lib/mocha');

it('should pass', () => {});

exports.mochaGlobalTeardown = async function () {
throw new Error('Teardown problem')
}
56 changes: 56 additions & 0 deletions test/integration/global-fixtures.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');

describe('Run Global Fixtures Error Handling', function () {

const FIXTURES_DIR = 'test/integration/fixtures/global-fixtures/';

function runMochaWithFixture(setupFile, testFile) {
return spawnSync('node', [
'bin/mocha',
'--require', path.join(FIXTURES_DIR, setupFile),
path.join(FIXTURES_DIR, testFile)
], { encoding: 'utf-8' });
}

function assertFailure(result, expectedMessage) {
assert.strictEqual(result.status, 1, 'Process should exit with 1');
assert(
result.stderr.includes(expectedMessage) || result.stdout.includes(expectedMessage),
`Should show error message: ${expectedMessage}`
);
}

it('should fail with non-zero exit code when global setup fails', function () {
const result = runMochaWithFixture('global-setup.fixture.js', 'failing-test.fixture.js');
assertFailure(result, 'Setup problem');
});

it('should fail with non-zero exit code when global teardown fails', function () {
const result = runMochaWithFixture('global-teardown.fixture.js', 'failing-test.fixture.js');
assertFailure(result, 'Teardown problem');
});

it('should combine failures with setup failures', function () {
const result = runMochaWithFixture('global-setup.fixture.js', 'failing-test.fixture.js');
assertFailure(result, 'Setup problem');
});

it('should combine failures with teardown failures', function () {
const result = runMochaWithFixture('global-teardown.fixture.js', 'failing-test.fixture.js');
assertFailure(result, 'Teardown problem');
});

it('should pass with zero exit code when no errors occur in setup', function () {
const result = runMochaWithFixture('passing-setup.fixture.js', 'test.fixture.js');
assert.strictEqual(result.status, 0, 'Process should exit with 0');
});

it('should pass with zero exit code when no errors occur in teardown', function () {
const result = runMochaWithFixture('passing-teardown.fixture.js', 'test.fixture.js');
assert.strictEqual(result.status, 0, 'Process should exit with 0');
});
});
102 changes: 102 additions & 0 deletions test/integration/global-teardown-errors.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict'

const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const fs = require('fs').promises;

describe('Global Teardown Error Handling', function() {
this.timeout(5000);

const setupFile = 'test/fixtures/global-teardown/setup.js';
const testFile = 'test/fixtures/global-teardown/test.js';

before(async function() {
await fs.mkdir(path.dirname(setupFile), { recursive: true });
Copy link
Member

Choose a reason for hiding this comment

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

[Refactor] Err, most fixture files are checked in to the repository. This would be the first instance in the test/ directory of fs.mkdir. -1 from me on this strategy of writing test files dynamically and then removing them after the tests are done. Please switch to having fixtures checked in as regular files.

Copy link
Member

Choose a reason for hiding this comment

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

@TG199 this piece of feedback is still open


await fs.writeFile(setupFile, `
exports.mochaGlobalTeardown = async function () {
throw new Error('Teardown failure');
};
`);

await fs.writeFile(testFile, `
describe('Test Suite', function() {
it('passing test', function() {
// This test passes
});
});
`);
});

after(async function() {
await fs.rm(path.dirname(setupFile), { recursive: true, force: true });
});

it('should fail with non-zero exit code when global teardown fails', function() {
const result = spawnSync('node', [
'bin/mocha',
'--require', setupFile,
testFile
], {
encoding: 'utf8'
});

assert.strictEqual(result.status, 1, 'Process should exit with code 1');

assert(result.stderr.includes('Teardown failure') ||
result.stdout.includes('Teardown failure'),
'Should show teardown error message');
});

it('should combine test failures with teardown failures', async function() {

await fs.writeFile(testFile, `
describe('Test Suite', function() {
it('failing test', function() {
throw new Error('Test failure');
});
});
`);

const result = spawnSync('node', [
'bin/mocha',
'--require', setupFile,
testFile
], {
encoding: 'utf8'
});

assert.strictEqual(result.status, 1, 'Process should exit with code 1');

const output = result.stdout + result.stderr;
assert(output.includes('Test failure'), 'Should show test error');
assert(output.includes('Teardown failure'), 'Should show teardown error');
});

it('should pass with zero exit code when no errors occur', async function() {
await fs.writeFile(setupFile, `
exports.mochaGlobalTeardown = async function () {
// Success case
};
`);

await fs.writeFile(testFile, `
describe('Test Suite', function() {
it('passing test', function() {
// This test passes
});
});
`);

const result = spawnSync('node', [
'bin/mocha',
'--require', setupFile,
testFile
], {
encoding: 'utf8'
});

assert.strictEqual(result.status, 0, 'Process should exit with code 0');
});
});
6 changes: 4 additions & 2 deletions test/unit/mocha.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,8 @@ describe('Mocha', function () {
await mocha.runGlobalSetup(context);
expect(mocha._runGlobalFixtures, 'to have a call satisfying', [
mocha.options.globalSetup,
context
context,
'Global Setup'
]);
});
});
Expand Down Expand Up @@ -1102,7 +1103,8 @@ describe('Mocha', function () {
await mocha.runGlobalTeardown();
expect(mocha._runGlobalFixtures, 'to have a call satisfying', [
mocha.options.globalTeardown,
context
context,
'Global Teardown'
]);
});
});
Expand Down