Skip to content

Commit 04df8a5

Browse files
committed
test_runner: share setup context with teardown
Pass a shared context object to both global hooks so setup and teardown can exchange state without return-value coupling. This keeps the API extensible and aligns the docs and fixtures with the ctx.server lifecycle pattern.
1 parent c2a0353 commit 04df8a5

File tree

5 files changed

+87
-10
lines changed

5 files changed

+87
-10
lines changed

doc/api/test.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -510,19 +510,24 @@ This module can export any of the following:
510510
* A `globalSetup` function which runs once before all tests start
511511
* A `globalTeardown` function which runs once after all tests complete
512512

513+
The same context object is passed as the first argument to both `globalSetup`
514+
and `globalTeardown`.
515+
513516
The module is specified using the `--test-global-setup` flag when running tests from the command line.
514517

515518
```cjs
516519
// setup-module.js
517-
async function globalSetup() {
520+
async function globalSetup(context) {
518521
// Setup shared resources, state, or environment
519522
console.log('Global setup executed');
523+
context.server = await startServer();
520524
// Run servers, create files, prepare databases, etc.
521525
}
522526

523-
async function globalTeardown() {
527+
async function globalTeardown(context) {
524528
// Clean up resources, state, or environment
525529
console.log('Global teardown executed');
530+
await context.server.close();
526531
// Close servers, remove files, disconnect from databases, etc.
527532
}
528533

@@ -531,15 +536,17 @@ module.exports = { globalSetup, globalTeardown };
531536

532537
```mjs
533538
// setup-module.mjs
534-
export async function globalSetup() {
539+
export async function globalSetup(context) {
535540
// Setup shared resources, state, or environment
536541
console.log('Global setup executed');
542+
context.server = await startServer();
537543
// Run servers, create files, prepare databases, etc.
538544
}
539545

540-
export async function globalTeardown() {
546+
export async function globalTeardown(context) {
541547
// Clean up resources, state, or environment
542548
console.log('Global teardown executed');
549+
await context.server.close();
543550
// Close servers, remove files, disconnect from databases, etc.
544551
}
545552
```

lib/internal/test_runner/harness.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function createTestTree(rootTestOptions, globalOptions) {
7171
counters: null,
7272
shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations),
7373
teardown: null,
74+
globalSetupContext: undefined,
7475
snapshotManager: null,
7576
previousRuns: null,
7677
isFilteringByName,
@@ -85,8 +86,12 @@ function createTestTree(rootTestOptions, globalOptions) {
8586
globalOptions.cwd,
8687
);
8788
harness.globalTeardownFunction = globalSetupFunctions.globalTeardownFunction;
89+
if (typeof globalSetupFunctions.globalSetupFunction === 'function' ||
90+
typeof globalSetupFunctions.globalTeardownFunction === 'function') {
91+
harness.globalSetupContext = { __proto__: null };
92+
}
8893
if (typeof globalSetupFunctions.globalSetupFunction === 'function') {
89-
return globalSetupFunctions.globalSetupFunction();
94+
await globalSetupFunctions.globalSetupFunction(harness.globalSetupContext);
9095
}
9196
return PromiseResolve();
9297
},
@@ -275,8 +280,9 @@ function setupProcessState(root, globalOptions) {
275280
kCancelledByParent));
276281

277282
if (root.harness.globalTeardownFunction) {
278-
await root.harness.globalTeardownFunction();
283+
await root.harness.globalTeardownFunction(root.harness.globalSetupContext);
279284
root.harness.globalTeardownFunction = null;
285+
root.harness.globalSetupContext = undefined;
280286
}
281287

282288
hook.disable();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const fs = require('node:fs');
4+
5+
const contextFlagPath = process.env.CONTEXT_FLAG_PATH;
6+
7+
async function startServer() {
8+
return {
9+
async close() {
10+
fs.writeFileSync(contextFlagPath, 'server closed');
11+
},
12+
};
13+
}
14+
15+
async function globalSetup(context) {
16+
context.server = await startServer();
17+
}
18+
19+
async function globalTeardown(context) {
20+
await context.server.close();
21+
}
22+
23+
module.exports = { globalSetup, globalTeardown };

test/parallel/test-runner-global-setup-teardown.mjs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,29 @@ async function runTest(
205205
assert.strictEqual(content, 'Teardown-only was executed');
206206
});
207207

208-
// TODO(pmarchini): We should be able to share context between setup and teardown
209-
it.todo('should share context between setup and teardown');
208+
it('should share context between setup and teardown', async () => {
209+
const contextFlagPath = tmpdir.resolve('context-shared.tmp');
210+
const setupFlagPath = tmpdir.resolve('setup-for-context.tmp');
211+
212+
// Create a setup file for test-file.js to find
213+
fs.writeFileSync(setupFlagPath, 'Setup was executed');
214+
215+
const { stdout } = await runTest({
216+
isolation,
217+
globalSetupFile: 'context-setup-teardown.js',
218+
env: {
219+
CONTEXT_FLAG_PATH: contextFlagPath,
220+
SETUP_FLAG_PATH: setupFlagPath,
221+
},
222+
runnerEnabled,
223+
});
224+
225+
assert.match(stdout, /pass 2/);
226+
assert.match(stdout, /fail 0/);
227+
assert.ok(fs.existsSync(contextFlagPath), 'Context flag file should exist');
228+
const content = fs.readFileSync(contextFlagPath, 'utf8');
229+
assert.strictEqual(content, 'server closed');
230+
});
210231

211232
it('should handle async setup and teardown', async () => {
212233
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');

test/parallel/test-runner-run-global-hooks.mjs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,28 @@ describe('require(\'node:test\').run with global hooks', { concurrency: false },
138138
assert.strictEqual(content, 'Teardown-only was executed');
139139
});
140140

141-
// TODO(pmarchini): We should be able to share context between setup and teardown
142-
it.todo('should share context between setup and teardown');
141+
it('should share context between setup and teardown', async () => {
142+
const contextFlagPath = tmpdir.resolve('context-shared.tmp');
143+
const setupFlagPath = tmpdir.resolve('setup-for-context.tmp');
144+
145+
// Create a setup file for test-file.js to find
146+
fs.writeFileSync(setupFlagPath, 'Setup was executed');
147+
148+
const { results } = await runTestWithGlobalHooks({
149+
globalSetupFile: 'context-setup-teardown.js',
150+
runnerEnv: {
151+
CONTEXT_FLAG_PATH: contextFlagPath,
152+
SETUP_FLAG_PATH: setupFlagPath,
153+
},
154+
isolation,
155+
});
156+
157+
assert.strictEqual(results.passed, 2);
158+
assert.strictEqual(results.failed, 0);
159+
assert.ok(fs.existsSync(contextFlagPath), 'Context flag file should exist');
160+
const content = fs.readFileSync(contextFlagPath, 'utf8');
161+
assert.strictEqual(content, 'server closed');
162+
});
143163

144164
it('should handle async setup and teardown', async () => {
145165
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');

0 commit comments

Comments
 (0)