Hone logo
Hone
Problems

Mastering Asynchronous Testing with jest.useFakeTimers in TypeScript

Testing asynchronous code, especially operations involving timeouts or intervals, can be notoriously difficult and time-consuming. jest.useFakeTimers is a powerful Jest feature that allows you to control time within your tests, making it possible to simulate the passage of time and test time-dependent logic reliably and efficiently. This challenge will guide you through implementing and testing a function that relies on setTimeout.

Problem Description

Your task is to create a TypeScript function called delayedGreeting that takes a name (string) and a delay (number in milliseconds) as arguments. This function should return a Promise that resolves with a greeting string "Hello, [name]!" after the specified delay.

Following this, you will write Jest tests for this delayedGreeting function. Critically, you must use jest.useFakeTimers() to control the execution of setTimeout within your tests. This will allow you to advance the timer and verify the function's behavior without actually waiting for the specified delay.

Key Requirements:

  1. delayedGreeting Function:

    • Accepts a name (string) and a delay (number, in milliseconds).
    • Returns a Promise<string>.
    • The Promise should resolve with the string "Hello, [name]!" after the specified delay.
    • If delay is 0 or negative, the Promise should resolve immediately.
  2. Jest Tests:

    • Use jest.useFakeTimers() to control setTimeout.
    • Test that the greeting is resolved after the specified delay.
    • Test that the greeting resolves immediately if the delay is 0 or negative.
    • Ensure all timers are cleared after each test using jest.useRealTimers() or jest.clearAllTimers().

Expected Behavior:

  • When delayedGreeting is called with a positive delay, the Promise should not resolve until the timer simulated by jest.advanceTimersByTime() has reached or exceeded that delay.
  • When delayedGreeting is called with a delay of 0 or less, the Promise should resolve immediately upon execution of the function.

Edge Cases to Consider:

  • What happens when delay is 0?
  • What happens when delay is a negative number?

Examples

Example 1: Testing a standard delay

// Function to be tested (delayedGreeting.ts)
export const delayedGreeting = (name: string, delay: number): Promise<string> => {
  return new Promise((resolve) => {
    if (delay <= 0) {
      resolve(`Hello, ${name}!`);
      return;
    }
    setTimeout(() => {
      resolve(`Hello, ${name}!`);
    }, delay);
  });
};

// Test file (delayedGreeting.test.ts)
import { delayedGreeting } from './delayedGreeting';

describe('delayedGreeting', () => {
  beforeEach(() => {
    jest.useFakeTimers(); // Enable fake timers
  });

  afterEach(() => {
    jest.useRealTimers(); // Restore real timers
    // or jest.clearAllTimers(); // If you only want to clear pending timers
  });

  test('should resolve with a greeting after the specified delay', async () => {
    const name = 'Alice';
    const delay = 1000;
    const promise = delayedGreeting(name, delay);

    // Advance timers by a value less than the delay
    jest.advanceTimersByTime(delay - 100);
    // At this point, the promise should NOT have resolved yet.
    // We can't directly check promise resolution state without awaiting,
    // so we'll focus on asserting after the full delay.

    // Advance timers to meet or exceed the delay
    jest.advanceTimersByTime(100); // Advance by the remaining 100ms

    const greeting = await promise;
    expect(greeting).toBe('Hello, Alice!');
  });
});

Explanation: In this test, we set up fake timers. We call delayedGreeting. We then advance the timers by delay - 100ms to simulate some time passing, but not enough for the setTimeout to trigger. Finally, we advance the timers by the remaining 100ms to ensure the setTimeout callback is executed. We then await the promise and assert its resolved value.

Example 2: Testing immediate resolution (delay = 0)

// Test file (delayedGreeting.test.ts)
// ... (previous setup and imports)

  test('should resolve immediately if delay is 0', async () => {
    const name = 'Bob';
    const delay = 0;
    const promise = delayedGreeting(name, delay);

    // No need to advance timers, as the delay is 0.
    // The promise should resolve synchronously or very close to it.

    const greeting = await promise;
    expect(greeting).toBe('Hello, Bob!');
  });

Explanation: When the delay is 0, the setTimeout callback would normally execute as soon as possible. With fake timers, the initial execution of delayedGreeting will handle the delay <= 0 condition and resolve the promise directly, without needing to advance timers.

Example 3: Testing immediate resolution (negative delay)

// Test file (delayedGreeting.test.ts)
// ... (previous setup and imports)

  test('should resolve immediately if delay is negative', async () => {
    const name = 'Charlie';
    const delay = -500;
    const promise = delayedGreeting(name, delay);

    // Similar to delay = 0, no timer advancement is needed.
    const greeting = await promise;
    expect(greeting).toBe('Hello, Charlie!');
  });

Explanation: The delayedGreeting function is designed to treat negative delays the same as a delay of 0, resolving the promise immediately. This test verifies that behavior.

Constraints

  • The name argument will always be a non-empty string.
  • The delay argument will be a number.
  • Tests should be efficient and not rely on actual time passing.
  • Your solution must use jest.useFakeTimers() and jest.advanceTimersByTime().

Notes

  • Remember that jest.useFakeTimers() by default mocks setTimeout, setInterval, clearTimeout, and clearInterval.
  • Consider what happens if you call jest.advanceTimersByTime() with a value less than the intended delay. The promise should not have resolved yet. You can assert this by checking if the promise has settled before advancing enough time. However, directly checking promise settlement state before await is tricky. The most common pattern is to await after advancing sufficient time and then assert the resolved value.
  • Using jest.useRealTimers() or jest.clearAllTimers() in afterEach or afterAll is crucial to prevent timer leaks and interference between tests.
  • Think about how to test the "not resolved yet" state. While await will pause execution until resolution, Jest offers utilities to check if a promise is pending, but for this challenge, focusing on the correct resolution after advancing time is sufficient.
Loading editor...
typescript