diff --git a/doc/api/test.md b/doc/api/test.md index 29d071e668b8c6..f1fcd38e572dc5 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1384,6 +1384,9 @@ added: - v18.9.0 - v16.19.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/61367 + description: Add the `env` option. - version: v24.7.0 pr-url: https://github.com/nodejs/node/pull/59443 description: Added a rerunFailuresFilePath option. @@ -1504,6 +1507,9 @@ changes: * `functionCoverage` {number} Require a minimum percent of covered functions. If code coverage does not reach the threshold specified, the process will exit with code `1`. **Default:** `0`. + * `env` {Object} Specify environment variables to be passed along to the test process. + This options is not compatible with `isolation='none'`. These variables will override + those from the main process, and are not merged with `process.env`. * Returns: {TestsStream} **Note:** `shard` is used to horizontally parallelize test running across diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 3f9e855f755212..01dd9483fff890 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -403,7 +403,7 @@ function runTestFile(path, filesWatcher, opts) { const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => { const args = getRunArgs(path, opts); const stdio = ['pipe', 'pipe', 'pipe']; - const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; + const env = { __proto__: null, NODE_TEST_CONTEXT: 'child-v8', ...(opts.env || process.env) }; if (watchMode) { stdio.push('ipc'); env.WATCH_REPORT_DEPENDENCIES = '1'; @@ -610,6 +610,7 @@ function run(options = kEmptyObject) { argv = [], cwd = process.cwd(), rerunFailuresFilePath, + env, } = options; if (files != null) { @@ -718,6 +719,14 @@ function run(options = kEmptyObject) { validatePath(globalSetupPath, 'options.globalSetupPath'); } + if (env != null) { + validateObject(env); + + if (isolation === 'none') { + throw new ERR_INVALID_ARG_VALUE('options.env', env, 'is not supported with isolation=\'none\''); + } + } + const rootTestOptions = { __proto__: null, concurrency, timeout, signal }; const globalOptions = { __proto__: null, @@ -763,6 +772,7 @@ function run(options = kEmptyObject) { argv, execArgv, rerunFailuresFilePath, + env, }; if (isolation === 'process') { diff --git a/test/fixtures/test-runner/process-env.js b/test/fixtures/test-runner/process-env.js new file mode 100644 index 00000000000000..6f936de450d87e --- /dev/null +++ b/test/fixtures/test-runner/process-env.js @@ -0,0 +1,7 @@ +const { test } = require('node:test'); + +test('process.env is correct', (t) => { + t.assert.strictEqual(process.env.ABC, undefined, 'main process env var should be undefined'); + t.assert.strictEqual(process.env.NODE_TEST_CONTEXT, 'child-v8', 'NODE_TEST_CONTEXT should be set by run()'); + t.assert.strictEqual(process.env.FOOBAR, 'FUZZBUZZ', 'specified env var should be defined'); +}); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 7ddd8c1dcd83e5..4d5980482e9da6 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -650,6 +650,26 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { }); }); +describe('env', () => { + it('should allow env variables to be configured', async () => { + // Set a variable on main process env and test it does not exist within test env. + process.env.ABC = 'XYZ'; + const stream = run({ files: [join(testFixtures, 'process-env.js')], env: { FOOBAR: 'FUZZBUZZ' } }); + stream.on('test:fail', common.mustNotCall()); + stream.on('test:pass', common.mustCall(1)); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + delete process.env.ABC; + }); + + it('should throw error when env is specified with isolation=none', async () => { + assert.throws(() => run({ env: { foo: 'bar' }, isolation: 'none' }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The property 'options\.env' is not supported with isolation='none'\. Received { foo: 'bar' }/ + }); + }); +}); + describe('forceExit', () => { it('throws for non-boolean values', () => { [Symbol(), {}, 0, 1, '1', Promise.resolve([])].forEach((forceExit) => {