node/test/parallel/test-runner-global-setup-teardown.mjs
Pietro Marchini 7e24ebc780
test_runner: unify --require and --import behavior when isolation none
PR-URL: https://github.com/nodejs/node/pull/57924
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
2025-05-06 06:28:29 +00:00

529 lines
19 KiB
JavaScript

import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import { spawn } from 'node:child_process';
import tmpdir from '../common/tmpdir.js';
import { once } from 'node:events';
import { join } from 'node:path';
const testFixtures = fixtures.path('test-runner');
async function runTest(
{
isolation,
globalSetupFile,
testFiles = ['test-file.js'],
env = {},
additionalFlags = [],
runnerEnabled = true,
}
) {
const globalSetupPath = join(testFixtures, 'global-setup-teardown', globalSetupFile);
const args = [];
if (runnerEnabled) {
args.push('--test');
}
if (isolation) {
args.push(`--test-isolation=${isolation}`);
}
args.push(
'--test-reporter=spec',
`--test-global-setup=${globalSetupPath}`
);
if (additionalFlags.length > 0) {
args.push(...additionalFlags);
}
const testFilePaths = testFiles.map((file) => join(testFixtures, 'global-setup-teardown', file));
args.push(...testFilePaths);
const child = spawn(
process.execPath,
args,
{
encoding: 'utf8',
stdio: 'pipe',
env: {
...process.env,
...env
}
}
);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
await once(child, 'exit');
return { stdout, stderr };
}
[
{
isolation: 'none',
runnerEnabled: true
},
{
isolation: 'process',
runnerEnabled: true
},
{
isolation: undefined,
runnerEnabled: false
},
].forEach((testCase) => {
const { isolation, runnerEnabled } = testCase;
describe(`test runner global hooks with isolation=${isolation} and --test: ${runnerEnabled}`, { concurrency: false }, () => {
beforeEach(() => {
tmpdir.refresh();
});
it('should run globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run setup-only module', async () => {
const setupOnlyFlagPath = tmpdir.resolve('setup-only-executed.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'setup-only.js',
env: {
SETUP_ONLY_FLAG_PATH: setupOnlyFlagPath,
SETUP_FLAG_PATH: setupOnlyFlagPath
},
runnerEnabled
});
assert.match(stdout, /Setup-only module executed/);
assert.ok(fs.existsSync(setupOnlyFlagPath), 'Setup-only flag file should exist');
const content = fs.readFileSync(setupOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Setup-only was executed');
});
it('should run teardown-only module', async () => {
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'teardown-only.js',
env: {
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Teardown-only module executed/);
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown-only was executed');
});
// Create a file in globalSetup and delete it in globalTeardown
// two test files should both verify that the file exists
// This works as the globalTeardown removes the setupFlag
it('should run globalTeardown only after all tests are done in case of more than one test file', async () => {
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'teardown-only.js',
env: {
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled,
testFiles: ['test-file.js', 'another-test-file.js']
});
if (runnerEnabled) {
assert.match(stdout, /pass 3/);
} else {
assert.match(stdout, /pass 2/);
}
assert.match(stdout, /fail 0/);
assert.match(stdout, /Teardown-only module executed/);
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown-only was executed');
});
// TODO(pmarchini): We should be able to share context between setup and teardown
it.todo('should share context between setup and teardown');
it('should handle async setup and teardown', async () => {
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-async.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'async-setup-teardown.js',
env: {
ASYNC_FLAG_PATH: asyncFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Async setup starting/);
assert.match(stdout, /Async setup completed/);
assert.match(stdout, /Async teardown starting/);
assert.match(stdout, /Async teardown completed/);
assert.ok(fs.existsSync(asyncFlagPath), 'Async flag file should exist');
const content = fs.readFileSync(asyncFlagPath, 'utf8');
assert.strictEqual(content, 'Setup part, Teardown part');
});
it('should handle error in setup', async () => {
const setupFlagPath = tmpdir.resolve('setup-for-error.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'error-in-setup.js',
env: {
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
// Verify that the error is reported properly
const errorReported = stderr.includes('Deliberate error in global setup');
assert.ok(errorReported, 'Should report the error from global setup');
// Verify the teardown wasn't executed
assert.ok(!stdout.includes('Teardown should not run if setup fails'),
'Teardown should not run after setup fails');
});
it('should run TypeScript globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-ts.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-ts.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.ts',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: ['--no-warnings'],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run ESM globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-esm.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-esm.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.mjs',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run globalSetup only once for run', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
// Global setup should run only once
assert.strictEqual(GlobalSetupOccurrences, 1);
});
it('should run globalSetup and globalTeardown only once for run', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
// Global teardown should run only once
assert.strictEqual(GlobalTeardownOccurrences, 1);
});
it('should run globalSetup and globalTeardown only once for run with multiple test files',
{
skip: !runnerEnabled ? 'Skipping test as --test is not enabled' : false
},
async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const testFiles = ['test-file.js', 'another-test-file.js'];
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled,
testFiles
});
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
assert.strictEqual(GlobalSetupOccurrences, 1);
assert.strictEqual(GlobalTeardownOccurrences, 1);
assert.match(stdout, /pass 3/);
assert.match(stdout, /fail 0/);
}
);
describe('interop with --require and --import', () => {
const cjsPath = join(testFixtures, 'global-setup-teardown', 'required-module.cjs');
const esmpFile = fixtures.fileURL('test-runner', 'global-setup-teardown', 'imported-module.mjs');
// The difference in behavior is due to how --require and --import are handled by
// the main entry point versus the test runner entry point.
// When isolation is 'none', both --require and --import are handled by the test runner.
const shouldRequireAfterSetup = runnerEnabled && isolation === 'none';
const shouldImportAfterSetup = runnerEnabled;
it(`should run required module ${shouldRequireAfterSetup ? 'after' : 'before'} globalSetup`, async () => {
const setupFlagPath = tmpdir.resolve('setup-for-required.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-for-required.tmp');
fs.writeFileSync(setupFlagPath, '');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
requirePath: './required-module.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--require=${cjsPath}`,
],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Required module executed/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
const requiredExecutedPosition = stdout.indexOf('Required module executed');
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
assert.ok(
shouldRequireAfterSetup ?
requiredExecutedPosition > globalSetupExecutedPosition :
requiredExecutedPosition < globalSetupExecutedPosition,
`Required module should have been executed ${shouldRequireAfterSetup ? 'after' : 'before'} global setup`
);
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it(`should run imported module ${shouldImportAfterSetup ? 'after' : 'before'} globalSetup`, async () => {
const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
fs.writeFileSync(setupFlagPath, 'non-empty');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.mjs',
importPath: './imported-module.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--import=${esmpFile}`,
],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Imported module executed/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
const importedExecutedPosition = stdout.indexOf('Imported module executed');
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
assert.ok(
shouldImportAfterSetup ?
importedExecutedPosition > globalSetupExecutedPosition :
importedExecutedPosition < globalSetupExecutedPosition,
`Imported module should have been executed ${shouldImportAfterSetup ? 'after' : 'before'} global setup`
);
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should execute globalSetup and globalTeardown correctly with imported module containing tests', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
const importedModuleWithTestFile = fixtures.fileURL(
'test-runner',
'global-setup-teardown',
'imported-module-with-test.mjs'
);
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'non-empty');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--import=${importedModuleWithTestFile}`,
],
runnerEnabled
});
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Imported module Ok/);
assert.match(stdout, /Imported module Fail/);
assert.match(stdout, /verify setup was executed/);
assert.match(stdout, /another simple test/);
assert.match(stdout, /Global teardown executed/);
assert.match(stdout, /tests 4/);
assert.match(stdout, /suites 0/);
assert.match(stdout, /pass 3/);
assert.match(stdout, /fail 1/);
assert.match(stdout, /cancelled 0/);
assert.match(stdout, /skipped 0/);
assert.match(stdout, /todo 0/);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
});
});
});