Hone logo
Hone
Problems

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 after delay milliseconds.
  • startUpdateChecker(interval: number): void: Starts a recurring check for updates every interval milliseconds. This method should call a provided checkForUpdates callback function.
  • stopUpdateChecker(): void: Stops the recurring update checks.

Your task is to:

  1. Mock timers: Use Jest's timer mocking utilities (jest.useFakeTimers(), jest.advanceTimersByTime(), jest.clearAllTimers()).
  2. Test scheduleNotification: Verify that a notification is scheduled and then delivered after the specified delay.
  3. Test startUpdateChecker and stopUpdateChecker: Verify that the checkForUpdates callback is called repeatedly at the specified interval and that stopUpdateChecker successfully halts these calls.

Expected Behavior:

  • When scheduleNotification is called, the notification callback should not execute immediately.
  • After advancing timers by the specified delay, the notification callback should execute exactly once.
  • When startUpdateChecker is called, the checkForUpdates callback should be invoked repeatedly.
  • When stopUpdateChecker is called, the checkForUpdates callback should cease to be invoked.

Edge Cases to Consider:

  • What happens if scheduleNotification is called with a delay of 0?
  • What happens if stopUpdateChecker is called before startUpdateChecker?
  • 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): mockCallback is 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): mockOnPoll has been called once.
  • Test Action 2: jest.advanceTimersByTime(1000);
  • Expected Outcome 2 (asserted in test): mockOnPoll has been called twice.
  • Test Action 3: stopPolling(intervalId); and jest.advanceTimersByTime(500);
  • Expected Outcome 3 (asserted in test): mockOnPoll is 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); (or jest.runAllTimers())
  • Expected Outcome (asserted in test): mockCallback is called.

Constraints

  • All tests must be written in TypeScript.
  • You must use Jest's timer mocking features.
  • Do not use setTimeout or setInterval directly 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 within beforeEach.
  • Remember to clean up your timers using jest.useRealTimers() or jest.clearAllTimers() in afterEach or afterAll.
  • 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() and jest.advanceTimersByTime() which can be useful depending on your testing strategy.
  • For setInterval, you'll need to advance timers multiple times to see multiple calls.
Loading editor...
typescript