-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Description
🚀 Feature Proposal
Expose an AbortSignal
(node, web) that emits when a test times out.
Motivation
When testing asynchronous operations, It's very easy to get errors like, ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down.
. To get clean test results, operations must be interrupted when the test times out.
Example
Naive implementation:
test('MyWritableStream should eventually process all records', async () => {
const data: Iterable<unknown> = generateTooMuchTestData(); // too much to process
const myWritableStream = new MyWritableStream();
await Promise.all([
pipeline(stream.Readable.from(data), myWritableStream);
runFakeTimersUntilStreamCloses(myWritableStream),
]);
expect(new Set(myWritableStream.processedData)).toStrictEqual(new Set(data));
}, 1, /* not enough time */);
If the test times out, the pipeline continues running:
ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From lib/stream.spec.ts.
270 | slow: 1,
271 | },
> 272 | },
| ^
273 | },
274 | ),
275 | 1,
at Timeout._onTimeout (lib/stream.spec.ts:272:22)
console.warn
A function to advance timers was called but the timers APIs are not replaced with fake timers. Call `jest.useFakeTimers()` in this test file or enable fake timers for all tests by setting 'fakeTimers': {'enableGlobally': true} in Jest configuration file.
Stack Trace:
270 | slow: 1,
271 | },
> 272 | },
| ^
273 | },
274 | ),
275 | 1,
Error
at FakeTimers._checkFakeTimers (../../node_modules/@jest/fake-timers/build/modernFakeTimers.js:158:13)
at FakeTimers.runAllTimers (../../node_modules/@jest/fake-timers/build/modernFakeTimers.js:49:14)
at Timeout._onTimeout (lib/stream.spec.ts:272:22)
270 | slow: 1,
271 | },
> 272 | },
| ^
273 | },
274 | ),
275 | 1,
at FakeTimers._checkFakeTimers (../../node_modules/@jest/fake-timers/build/modernFakeTimers.js:152:28)
at FakeTimers.runAllTimers (../../node_modules/@jest/fake-timers/build/modernFakeTimers.js:49:14)
at Timeout._onTimeout (lib/stream.spec.ts:272:22)
ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From lib/stream.spec.ts.
This ought to fix it, but seems like a kludge:
const streamsToCleanUp = [];
function cleanUpStreams() {
streamsToCleanUp.splice(0, streamsToCleanUp.length).forEach(s => {
s.destroy();
});
}
afterEach(cleanUpStreams);
test('MyWritableStream should eventually process all records', async () => {
const data: Iterable<unknown> = generateTooMuchTestData(); // too much to process
const source = stream.Readable.from(data);
const myWritableStream = new MyWritableStream();
streamsToCleanUp.push(source);
streamsToCleanUp.push(myWritableStream);
try {
await Promise.all([
pipeline(source, myWritableStream);
runFakeTimersUntilStreamCloses(myWritableStream),
]);
expect(new Set(myWritableStream.processedData)).toStrictEqual(new Set(data));
} finally {
cleanUpStreams();
}
}, 1 /* not enough time */);
Better solution:
test('MyWritableStream should eventually process all records', async () => {
const data: Iterable<unknown> = generateTooMuchTestData(); // too much to process
const source = stream.Readable.from(data);
const myWritableStream = new MyWritableStream();
jest.abortSignal.on('abort', () => { // use some hypothetical 'jest.abortSignal' API
source.destroy();
myWritableStream.destroy();
});
await Promise.all([
pipeline(source, myWritableStream);
runFakeTimersUntilStreamCloses(myWritableStream),
]);
expect(new Set(myWritableStream.processedData)).toStrictEqual(new Set(data));
}, 1, /* not enough time */);
The API jest.abortSignal
used in this example is illustrative only -- I'm not sure what would actually be the best API design for this.
Please excuse any mistakes in the example code -- it is only meant to illustrate the use case and has not been tested.
Pitch
The current best practice, it seems, is to duplicate the timeout within the test itself, or maintain operations that need to be interrupted in some a broader scope and clean it up in afterEach. It would be cleaner to subscribe to an
abort` event from Jest and thread it through to the code under test.