Mastering Jest Timers: A Time-Bending Challenge
Testing asynchronous code involving timers like setTimeout and setInterval can be tricky. Jest provides powerful tools to control time, allowing you to precisely manage and test time-dependent logic without waiting for real-world delays. This challenge will test your ability to leverage Jest's timer mocks to effectively test functions that rely on these time-based operations.
Problem Description
You are tasked with testing a NotificationService class that uses setTimeout to schedule notifications and setInterval to periodically check for updates. Your goal is to write Jest tests that can accurately verify the behavior of these timer-dependent operations without actually waiting for the timers to elapse.
The NotificationService has the following methods:
scheduleNotification(message: string, delay: number): void: Schedules a notification to be displayed afterdelaymilliseconds.startUpdateChecker(interval: number): void: Starts a recurring check for updates everyintervalmilliseconds. This method should call a providedcheckForUpdatescallback function.stopUpdateChecker(): void: Stops the recurring update checks.
Your task is to:
- Mock timers: Use Jest's timer mocking utilities (
jest.useFakeTimers(),jest.advanceTimersByTime(),jest.clearAllTimers()). - Test
scheduleNotification: Verify that a notification is scheduled and then delivered after the specified delay. - Test
startUpdateCheckerandstopUpdateChecker: Verify that thecheckForUpdatescallback is called repeatedly at the specified interval and thatstopUpdateCheckersuccessfully halts these calls.
Expected Behavior:
- When
scheduleNotificationis called, the notification callback should not execute immediately. - After advancing timers by the specified delay, the notification callback should execute exactly once.
- When
startUpdateCheckeris called, thecheckForUpdatescallback should be invoked repeatedly. - When
stopUpdateCheckeris called, thecheckForUpdatescallback should cease to be invoked.
Edge Cases to Consider:
- What happens if
scheduleNotificationis called with a delay of 0? - What happens if
stopUpdateCheckeris called beforestartUpdateChecker? - Ensure that timer mocks are properly managed between tests (e.g., cleaning up).
Examples
Let's consider a simplified scenario for scheduleNotification:
Example 1:
// Function to test
function notifyAfterDelay(message: string, delay: number, callback: (msg: string) => void) {
setTimeout(() => {
callback(message);
}, delay);
}
// Test scenario
// Initial state: No notification delivered.
// After advancing timers by 1000ms: Notification is delivered.
// Expected output of tests would assert the callback being called after timer advancement.
- Input (for the test): Call
notifyAfterDelay("Hello!", 1000, mockCallback). - Test Action:
jest.advanceTimersByTime(1000); - Expected Outcome (asserted in test):
mockCallbackis called with"Hello!".
Example 2:
// Function to test
function startPolling(interval: number, onPoll: () => void) {
const intervalId = setInterval(onPoll, interval);
return intervalId;
}
function stopPolling(intervalId: NodeJS.Timeout) {
clearInterval(intervalId);
}
// Test scenario
// Initial state: No polling occurred.
// After advancing timers by 500ms: 'onPoll' has been called once.
// After advancing timers by another 1000ms (total 1500ms): 'onPoll' has been called twice.
// After calling stopPolling: 'onPoll' stops being called.
// Expected output of tests would assert the number of times 'onPoll' is called.
- Input (for the test): Call
startPolling(500, mockOnPoll). - Test Action 1:
jest.advanceTimersByTime(500); - Expected Outcome 1 (asserted in test):
mockOnPollhas been called once. - Test Action 2:
jest.advanceTimersByTime(1000); - Expected Outcome 2 (asserted in test):
mockOnPollhas been called twice. - Test Action 3:
stopPolling(intervalId);andjest.advanceTimersByTime(500); - Expected Outcome 3 (asserted in test):
mockOnPollis still called only twice (no new calls after stopping).
Example 3: Edge Case - Zero Delay
// Function to test
function notifyImmediately(callback: () => void) {
setTimeout(callback, 0);
}
// Test scenario
// Initial state: Callback not executed.
// After advancing timers by 0ms (or a very small amount if needed): Callback is executed.
// Expected outcome of tests would assert the callback being called after timer advancement.
- Input (for the test): Call
notifyImmediately(mockCallback). - Test Action:
jest.advanceTimersByTime(0);(orjest.runAllTimers()) - Expected Outcome (asserted in test):
mockCallbackis called.
Constraints
- All tests must be written in TypeScript.
- You must use Jest's timer mocking features.
- Do not use
setTimeoutorsetIntervaldirectly in your tests. - Ensure that timer mocks are reset or cleared appropriately between test cases to prevent interference.
Notes
- Consider using
jest.useFakeTimers()at the beginning of your test suite or withinbeforeEach. - Remember to clean up your timers using
jest.useRealTimers()orjest.clearAllTimers()inafterEachorafterAll. - Think about how you can spy on or mock the callbacks to assert that they are being invoked correctly.
- Jest provides methods like
jest.runAllTimers()andjest.advanceTimersByTime()which can be useful depending on your testing strategy. - For
setInterval, you'll need to advance timers multiple times to see multiple calls.