CodePark/ questions / Mocks, spies & timers
START PRACTICINGPRACTICE
Frontend question · Testing › Mocks, spies & timers

How would you test a search box that calls onSearch 300ms after the user stops typing (debounced)?

beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());

test("searches 300ms after typing stops", async () => {
  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
  const onSearch = jest.fn();
  render(<SearchBox onSearch={onSearch} />);
  await user.type(screen.getByRole("searchbox"), "react");
  act(() => jest.advanceTimersByTime(299));
  expect(onSearch).not.toHaveBeenCalled();
  act(() => jest.advanceTimersByTime(1));
  expect(onSearch).toHaveBeenCalledTimes(1);
  expect(onSearch).toHaveBeenCalledWith("react");
});

Use fake timers so the test controls time, instead of waiting for real delays.

The setup:

  • Call jest.useFakeTimers(), or vi.useFakeTimers() in Vitest.
  • Create user-event with userEvent.setup({ advanceTimers: jest.advanceTimersByTime }). Then user-event's own internal delays move with the fake clock.

The test:

  • Type a query.
  • Advance the clock to just before the 300ms delay. Assert onSearch was not called.
  • Advance the last millisecond. Assert onSearch was called once, with the final value.
  • Wrap timer advances in act when they trigger React state updates.
  • Restore real timers in afterEach with jest.useRealTimers(), so other tests aren't affected.

This tests the behaviour: called once, after the pause, with the last value. It doesn't depend on how the debounce is implemented.

  • Turn on fake timers with jest.useFakeTimers() or vi.useFakeTimers().
  • Use userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) so user-event follows the fake clock.
  • Assert it was not called before the delay, and called once after it.
  • Wrap timer advances that update state in act.
  • Restore real timers after each test.