When writing unit tests, it’s super important to isolate the code under test from its external dependencies. Dependencies could include services like databases, external APIs, file systems, or third-party libraries that you don’t want to interact with directly in your tests. By mocking these dependencies, you can simulate their behavior, control their output, and ensure that your tests remain fast, reliable, and focused on the logic being tested.
Why Mock Dependencies?
- Isolation: By mocking dependencies, you can test your code in isolation without relying on external services or systems.
- Control: You can control the behavior of dependencies, such as simulating errors, returning specific data, or testing different scenarios.
- Performance: Mocking allows you to avoid the overhead of interacting with slow or unavailable services like databases or network calls.
- Repeatability: Mocks ensure consistent behavior, making tests more predictable and easier to debug when they fail.
How to Mock Dependencies in Vitest
Vitest provides powerful methods like vi.fn()
and vi.mock()
to mock functions, modules, and services that your code depends on. You can mock individual functions, or you can mock entire modules to replace their real implementations during testing.
If we go back to our log
function in examples/logjam
—you’ll see that in production, we send the log to the “server” if we’re in production. sendToServer
is a no-op because in a previous example, I didn’t want our test to blow up, but you can imagine that it’s calling fetch
or something. And, you don’t particularly want that happening every time you run your tests. So, we can choose to mock out that function instead.
vi.mock('./send-to-server', {
sendToServer: vi.fn(),
});
vi.mock()
takes a path fro a dependency and let’s you replace that functionality with a mock.
Now, we can run our tests with that function mocked out.
beforeEach(() => {
vi.stubEnv('MODE', 'production');
});
afterEach(() => {
vi.unstubAllEnvs();
vi.resetAllMocks();
});
it('sends messages to the server in production mode', () => {
log('Hello, world!');
expect(sendToServer).toHaveBeenCalledWith('info', 'Hello, world!');
});
Auto-Mocking
If all you’re doing is mocking out every function in a module, you can just use Vite’s Auto-Mocking functionality.
vi.mock('./send-to-server');
This will do the same thing as the above example. The auto-mocking algorithm works something like this:
- All arrays will be emptied.
- All primitives and collections will stay the same.
- All objects will be deeply cloned.
- All instances of classes and their prototypes will be deeply cloned.
Mocking Individual Functions
If your code depends on individual functions from a service or module, you can mock those specific functions using vi.fn()
to simulate their behavior.
Here’s an example of mocking a database service function:
// Database service that will be mocked
function fetchBandData(bandName) {
// Normally this would fetch from a database
return database.query(`SELECT * FROM bands WHERE name = '${bandName}'`);
}
// Code under test
async function getBandDetails(bandName) {
return await fetchBandData(bandName);
}
describe('getBandDetails', () => {
it('should return mocked band data', async () => {
// Mock fetchBandData to simulate database response
const mockFetchBandData = vi.fn(() =>
Promise.resolve({ name: 'Green Day', genre: 'Punk Rock' }),
);
// Call the function with the mocked dependency
const result = await mockFetchBandData('Green Day');
// Check that the function was called with the correct argument
expect(mockFetchBandData).toHaveBeenCalledWith('Green Day');
// Assert the return value
expect(result).toEqual({ name: 'Green Day', genre: 'Punk Rock' });
});
});
In this example, we replace the real fetchBandData
function with a mock implementation that simulates a database call. This ensures that our test runs quickly and in isolation.
Mocking Entire Modules
In cases where your code depends on multiple functions from a module or service, you can mock the entire module using vi.mock()
. This is particularly useful for mocking external libraries or services that provide multiple functionalities.
Here’s how to mock an entire module:
// Imagine you have a module called 'api' with various functions
import * as api from './api';
// Code under test
async function getConcertDetails(bandName) {
return await api.fetchConcerts(bandName);
}
describe('getConcertDetails', () => {
// Mock the entire api module
vi.mock('./api', () => ({
fetchConcerts: vi.fn(() =>
Promise.resolve([{ venue: 'Madison Square Garden', date: '2024-09-01' }]),
),
}));
it('should return mocked concert details', async () => {
// Call the function with the mocked module
const result = await getConcertDetails('Green Day');
// Check that the fetchConcerts mock was called
expect(api.fetchConcerts).toHaveBeenCalledWith('Green Day');
// Assert the return value
expect(result).toEqual([{ venue: 'Madison Square Garden', date: '2024-09-01' }]);
});
});
In this example, the entire api
module is mocked, and the fetchConcerts
function is replaced with a mock that returns predefined concert data. This allows the test to run without making real API calls.
Mocking Node.js Built-in Modules
You can also mock built-in Node.js modules like fs
or http
if your code interacts with the file system or performs network requests.
Here’s an example of mocking the fs
module:
import * as fs from 'fs';
// Code under test
function readConfigFile(filePath) {
return fs.readFileSync(filePath, 'utf-8');
}
describe('readConfigFile', () => {
// Mock the fs module
vi.mock('fs');
it('should read the mocked config file', () => {
// Mock the fs.readFileSync method
vi.spyOn(fs, 'readFileSync').mockReturnValue('mocked file content');
// Call the function under test
const result = readConfigFile('/path/to/config');
// Assert the returned file content
expect(result).toBe('mocked file content');
});
});
In this test, the fs.readFileSync
method is mocked to return predefined content, allowing you to test your function without needing an actual file on the file system.
Example: Mocking an External API
Here’s a complete example of mocking an external API dependency using Vitest:
// Assume you have an API module
import * as api from './api';
// Code under test
async function fetchConcertData(bandName) {
const response = await api.getConcerts(bandName);
return response.data;
}
describe('fetchConcertData', () => {
// Mock the API module
vi.mock('./api', () => ({
getConcerts: vi.fn(() => Promise.resolve({ data: [{ venue: 'Wembley', date: '2024-10-05' }] })),
}));
it('should fetch concert data using the mocked API', async () => {
// Call the function under test
const result = await fetchConcertData('Green Day');
// Assert that the API was called with the correct argument
expect(api.getConcerts).toHaveBeenCalledWith('Green Day');
// Check the returned data
expect(result).toEqual([{ venue: 'Wembley', date: '2024-10-05' }]);
});
});
In this example, we mock the entire API module, ensuring the test doesn’t make any real API requests. The mock returns predefined data, allowing you to control the test scenario completely.
Whether you’re mocking APIs, databases, or file systems, mocking ensures that your tests remain predictable, fast, and focused on the logic under test. Always, always, always remember to reset or restore mocks after tests to maintain a clean and consistent test environment.
vi.doMock
vi.doMock
is basically the same as vi.mock
except for the fact that it’s not hoisted to the top, which means you have access to variables. The next import of that module will be mocked.