Code that depends on time cannot be tested head-on. A search box does not fire a request on every keystroke - it waits for a 300 millisecond pause and only then sends the last value. That is debounce. An access token lives for an hour and then expires. In all of these the behavior is driven not by the input but by the passage of time, and an ordinary test either fails to wait for the right moment or catches a random result.
The naive answer is to wait for real: set a 300 ms setTimeout in the test and check the result afterwards. It feels natural, since that is how production works. But real waiting turns a fast unit test into a slow one, and the sum of such pauses stretches the run into minutes. Worse, the test turns flaky: on a loaded CI runner the timer fires later, and a check timed to the edge fails at random.
The mechanism that removes this uncertainty is fake timers. Calling jest.useFakeTimers swaps the global setTimeout, setInterval and Date for controllable doubles from the @sinonjs/fake-timers library, so time no longer moves on its own - you move it by hand. The modern implementation became the default back in Jest 27 and remains so in Jest 30, where the library was updated and advanceTimersToNextFrame was added for animation frames. You need not opt in with a separate option.
From there you drive the clock explicitly. jest.advanceTimersByTime(300) pushes the fake clock 300 ms forward and synchronously fires every timer whose deadline has arrived. In a debounce test this is the key move: you call the wrapped function twice, wind time forward by 300 ms, and confirm the real search went out exactly once and with the last value. No real pauses - the test is instant and identical on any machine.
Fake clocks are a global override, so they must be rolled back. useFakeTimers in beforeEach and useRealTimers in afterEach guarantee that the next test starts with real time rather than with someone else's timers. When you need to pin a specific moment - say, to check that a token issued at 10:00 expires at 11:00 - jest.setSystemTime sets the exact date; this function exists only in the modern implementation and is unavailable in the legacy one.
It is worth drawing a line here. Fake timers are good for scheduler behavior - debounce, throttle, polling, retries. But when domain logic itself asks what day it is, overriding the global clock for that is crude. It is cleaner to inject a Clock: a tiny dependency with a now() method, implemented by the system in production and by a function returning the needed date in a test. That way the date becomes an ordinary input rather than hidden global state.
Recursive timers demand special care. A poll that schedules the next setTimeout at the end of every step never finishes, and a call to jest.runAllTimers will spin an infinite loop until the test hangs. For such cases advance time in measured steps through advanceTimersByTime, or run only the already queued timers through runOnlyPendingTimers.
The mechanism has its price. A forgotten useRealTimers poisons neighboring tests; a mix of timers and promises calls for care - move the clock, then await the microtasks. In return, refusing fake timers gives you the classic flake: a test with a real 300 ms wait passes on the developer's fast machine and times out on a slow CI. Deterministic clocks turn time from a source of randomness into one more controlled input of the test.
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2026-07-26T10:00:00Z'))
})
afterEach(() => jest.useRealTimers())
test('fires search after 300 ms debounce', async () => {
const search = jest.fn()
const debounced = debounce(search, 300)
debounced('je')
debounced('jest')
jest.advanceTimersByTime(300)
expect(search).toHaveBeenCalledTimes(1)
expect(search).toHaveBeenCalledWith('jest')
})