A lot of UIs tend to show stuff like time and dates. As we’ve discussed previously, we want our tests to be consistent. As Steve Miller once wrote, (but let’s be honest, you ‘re thinking of Seal’s version from the Space Jam soundtrack):
Time keeps on slipping into the future. Time keeps on slipping, slipping, slipping into the future.
Under the hood, Vitest uses @sinonjs/fake-timers
.
Typically, if you need to control time in your tests, you’d opt in to using Sinon’s fake timers before the test suite in question and then you’d be a good time traveler and try to put everything back the way you found it when you’re all done.
beforeEach(() => {
// Take control of time.
vi.useFakeTimers();
});
afterEach(() => {
// Put things back the way you found it.
vi.useRealTimers();
});
useFakeTimers()
replaces the global setTimeout
, clearTimeout
, setInterval
, setImmediate
, clearImmediate
, process.hrtime
, performance.now
, and Date
with a custom implementation that you can control.
It returns a clock
object that starts at the Unix epoch (i.e. 0
). If you want to start time at some other point, you can pass it a different integer, but I’m going to argue that you’re better off using setSystemTime
, as we’ll see below.
vi.useFakeTimers(1677952591024);
Time is also effectively frozen unless you choose to advance it yourself. If you want time to move forward as it normally does, you can pass a option to useFakeTimers()
.
vi.useFakeTimers({ shouldAdvanceTime: true });
Manipulating time
Setting the Time
Now in any test, you can manually set the time to whatever you need it to be.
const date = new Date(2012, 1, 1, 13);
vi.setSystemTime(date);
- You can get access to the mocked time using
vi.getMockedSystemTime()
. - You can get access to the real time using
vi.getRealSystemTime()
. (I cannot even come up with a reason why you’d want to do this. I’m just mentioning it in the name of completeness).
Advancing Time Forward
These are helpful when setting timers like setInterval
and setTimeout
.
vi.advanceTimersByTime
,vi.advanceTimersByTimeAsync
: Moves the current time forward by a specified number of milliseconds.vi.advanceTimersToNextTimer
,vi.advanceTimersToNextTimerAsync
: Advances time until the next timer is fired.vi.getTimerCount
: Returns a count of the number of remaining timers.vi.runAllTimers
,vi.runAllTimersAsync
: Run all of the timers. (This one will throw an an error at 10,000 tries if you have asetInterval
that is never cleared.)vi.runAllTicks
: Call every microtask created byprocess.nextTick
.
Cleaning Up
vi.clearAllTimers
: Removes any timers that are scheduled to run.vi.restoreCurrentDate
: Put the originalDate
object back where it belongs.vi.useRealTimers
: When all of your timers have run out, this method will return all of your mocked timers back to their original implementations.
Mocking Timers, Dates, and other System Utilities
In some cases, your code may rely on system utilities like timers or dates, which can introduce unpredictability into your tests. Vitest provides built-in functionality to mock and control system utilities like setTimeout
, setInterval
, and Date
to ensure your tests behave consistently.
Mocking Timers
When dealing with timers, such as setTimeout
or setInterval
, you can mock them to control their behavior in tests using vi.useFakeTimers()
.
// Mock the timers
vi.useFakeTimers();
// Example function that uses setTimeout
function delay(callback) {
setTimeout(() => {
callback('Delayed');
}, 1000);
}
describe('delay function', () => {
it('should call callback after delay', () => {
const callback = vi.fn();
// Call the function under test
delay(callback);
// Fast-forward time
vi.advanceTimersByTime(1000);
// Assert that the callback was called
expect(callback).toHaveBeenCalledWith('Delayed');
});
});
In this example, vi.useFakeTimers()
allows us to take control of the timers and fast-forward time with vi.advanceTimersByTime(1000)
to simulate the passing of time without waiting for real-world delays.
Mocking Dates
Similarly, you can mock the Date
constructor to return specific dates or times.
// Mock the current date to always return a specific date
const mockDate = new Date(2024, 1, 1);
vi.setSystemTime(mockDate);
describe('mocked Date', () => {
it('should return the mocked date', () => {
const currentDate = new Date();
expect(currentDate).toEqual(mockDate);
});
});
By using vi.setSystemTime()
, you can ensure that new Date()
always returns a consistent value, making tests that rely on dates predictable.
Mocking Modules and Resetting Mocks
In more complex systems, you may want to mock entire modules to isolate the code under test. Vitest allows you to mock specific modules with vi.mock()
, which provides flexibility in controlling the behavior of dependencies.
// Mock the api module
vi.mock('./api', () => ({
getConcertDetails: vi
.fn()
.mockResolvedValue({ band: 'Green Day', venue: 'Madison Square Garden' }),
}));
This allows you to mock all the functions inside the api
module at once. When testing, this mocked version will replace the real module, ensuring you can simulate various scenarios.
Resetting Mocks
After running a test, it’s important to reset or restore the mocks to their original state to avoid cross-test interference. You can reset mocks using vi.resetAllMocks()
or restore original implementations with mockRestore()
.
// Reset all mocks after each test
afterEach(() => {
vi.resetAllMocks();
});
This ensures that each test starts with fresh mocks and eliminates unintended behavior caused by shared state between tests.
Combining Mocks with Spies and Stubs
You can combine the functionality of mocks, spies, and stubs for more advanced test scenarios. For example, you can mock a function and also spy on its interactions or stub a method inside a mock.
// Create a mock and spy on one of its methods
const mockObject = {
method: vi.fn().mockReturnValue('Mocked result'),
};
// Spy on the method
const spy = vi.spyOn(mockObject, 'method');
mockObject.method('argument');
// Verify the interaction
expect(spy).toHaveBeenCalledWith('argument');
expect(spy).toHaveReturnedWith('Mocked result');
This example combines a mock and a spy, allowing you to control the behavior of the method and also observe how it is used within the test.
Example: Mocking Timeouts or Asynchronous Functions like setTimeout
Mocking asynchronous functions like setTimeout
is another advanced use case. By mocking timers, you can simulate asynchronous behavior without actually waiting for the real delay.
Here’s an example where we mock setTimeout
and use vi.useFakeTimers()
to control the flow of time in an asynchronous function:
function delayedFunction(callback) {
setTimeout(() => {
callback('Done');
}, 3000);
}
describe('delayedFunction', () => {
it('should call callback after timeout', () => {
// Mock the timer
vi.useFakeTimers();
const callback = vi.fn();
// Call the function under test
delayedFunction(callback);
// Fast-forward the timer
vi.advanceTimersByTime(3000);
// Assert that the callback was called
expect(callback).toHaveBeenCalledWith('Done');
});
});
In this test, the timer is controlled using vi.useFakeTimers()
and vi.advanceTimersByTime()
, which simulates the setTimeout
without waiting for the actual delay. This allows you to test asynchronous code in a controlled and predictable way.