Timers
Loading "Timers (π solution)"
Run locally for transcripts
I begin from setting up the testing hooks to mock time in this test, using the
vi.useFakeTimers()
and vi.useRealTimers()
functions, the same way I did in the previous exercise:beforeAll(() => {
vi.useFakeTimers()
})
afterAll(() => {
vi.useRealTimers()
})
Note that mocking date and time includes mocking timers and intervals as well!
My next goal is to call the
debouncedFn()
and make sure that the wrapped fn
function is not called since the debounce duration has not yet passed.test('executes the callback after the debounce timeout passes', () => {
const fn = vi.fn<(input: string) => void>()
const debouncedFn = debounce(fn, 250)
debouncedFn('one')
expect(fn).not.toHaveBeenCalled()
Since thedebounce()
accepts any function as the argument, I am creating a mock function viavi.fn()
so I can spy on its calls and assert them! Passing it a type argument of(input: number) => void
also allows me to define the call signature of that mocked function to keep it type-safe in tests.
Now, let's shift the time!
Vitest provides us with a bunch of utility functions to control timers in test:
vi.advanceTimersByTime()
vi.advanceTimersByTimeAsync()
vi.advanceTimersToNextTimer()
vi.advanceTimersToNextTimerAsync()
All of those are great tools to have in your toolbelt. In this test, I will use the
vi.advanceTimersByTime()
function to advance the time in test by 250 milliseconds.vi.advanceTimersByTime(250)
This will immediately fastforward the test universe ahead by 250ms, allowing me to write my expectations toward the tested debounced function:
vi.advanceTimersByTime(250)
expect(fn).toHaveBeenCalledOnce()
expect(fn).toHaveBeenCalledWith('one')
The second test case will employ everything we've learned thus far to test the repetitive calls to the debounced function:
test('debounces the callback until the timeout passes since the last call', () => {
const fn = vi.fn<(input: string) => void>()
const debouncedFn = debounce(fn, 250)
debouncedFn('one')
expect(fn).not.toHaveBeenCalled()
vi.advanceTimersByTime(100)
debouncedFn('two')
expect(fn).not.toHaveBeenCalled()
vi.advanceTimersByTime(250)
expect(fn).toHaveBeenCalledOnce()
expect(fn).toHaveBeenCalledWith('two')
})
Note that you can use fake timers andvi.advanceTimersByTime()
andvi.advanceTimersToNextTimer()
to test intervals (setInterval
) too!
sleep()
Difference with You may be wondering about the difference between
vi.advanceTimersByTime()
and something like a sleep()
function. Since in this case we do need to wait for a fixed period of time to pass, using sleep()
may seem like a viable alternative. But the two behave completely differently.The
vi.advanceTimersByTime()
function advances the mocked time immediately. You can advance the timers by a year and only a millisecond will pass in your test (this is not an Interstellar reference). It allows you to be in full control over the time in your test, separating the mocked and the real time.The
sleep()
function, on the other hand, will actually wait for the given time. So if your functionality has a timeout that exceeds your test's timeout, you won't be able to sleep it through. From this perspective, sleep()
becomes a part of the action in your test, not the setup.It's worth mentioning that sincesleep()
often depends onsetTimeout()
, it won't do anything if you are using fake timers anyway. You would have to advance the timers for thesleep()
function as well for it to work.