Mocking Modules and Dependencies

· 5 min read · Updated March 7, 2026 · intermediate
mocking jest vitest dependencies unit-tests intermediate

When testing JavaScript code, you often need to isolate the unit under test from its dependencies. Database calls, API requests, file system operations, and third-party libraries are all examples of dependencies that can make tests slow, flaky, or impossible to run in isolation. This is where mocking comes in.

In this tutorial, you’ll learn how to mock modules, functions, and entire dependencies using Jest and Vitest. We’ll cover function mocks, module mocking, spies, and timer faking—everything you need to write fast, reliable, isolated tests.

Why Mocking Matters

Consider this function that fetches user data from an API:

// userService.js
const axios = require('axios');

async function getUserById(id) {
  const response = await axios.get(`/api/users/${id}`);
  return response.data;
}

module.exports = { getUserById };

To test this function without mocking, you’d need a running API server. This makes your tests:

  • Slow — network requests take time
  • Flaky — network issues cause random failures
  • Hard to maintain — tests depend on external state

Mocking solves all three problems by replacing real dependencies with controlled test doubles.

Mocking Functions

The most basic form of mocking is replacing a single function with a mock implementation.

Creating Mock Functions

Both Jest and Vitest provide a way to create mock functions:

// Jest
const mockFn = jest.fn();

// Vitest
const mockFn = vi.fn();

You can provide a default return value:

const mockFn = jest.fn(() => 'default value');
mockFn(); // 'default value'

Or use mockReturnValue/mockReturnValueOnce for more control:

const mockFn = jest.fn()
  .mockReturnValue('first')
  .mockReturnValue('second')
  .mockReturnValue('default');

mockFn(); // 'first'
mockFn(); // 'second'
mockFn(); // 'default'
mockFn(); // 'default'

Mocking Implementation

For functions that need to perform calculations or transformations:

const calculateDiscount = jest.fn((price, rate) => {
  return price * (rate / 100);
});

calculateDiscount(100, 20); // 20
calculateDiscount; // Mock function

Mocking Modules

When your code imports a module, you can mock the entire module. This is powerful for replacing third-party libraries like axios, lodash, or any npm package.

Using jest.mock()

// api.js
const axios = require('axios');

module.exports = {
  getUser: async (id) => {
    const response = await axios.get(`/users/${id}`);
    return response.data;
  }
};
// api.test.js
const api = require('./api');
const axios = require('axios');

// Mock the entire axios module
jest.mock('axios');
const mockedAxios = axios;

test('fetches user successfully', async () => {
  // Configure the mock
  mockedAxios.get.mockResolvedValue({
    data: { id: 1, name: 'Alice', email: 'alice@example.com' }
  });

  const user = await api.getUser(1);

  expect(user).toEqual({
    id: 1,
    name: 'Alice',
    email: 'alice@example.com'
  });
  expect(mockedAxios.get).toHaveBeenCalledWith('/users/1');
});

The jest.mock() call is hoisted to the top of the file, so it runs before any imports. This means you can mock modules that are imported elsewhere in your test file.

Mocking with doMock (ES Modules)

If you’re using ES modules with import, use jest.doMock() instead:

// ES module version
jest.doMock('./api', () => ({
  getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}));

import { getUser } from './api';

Partial Module Mocking

Sometimes you only want to mock part of a module while keeping the rest real:

jest.mock('lodash', () => ({
  ...jest.requireActual('lodash'),
  debounce: vi.fn((fn) => fn) // Replace only debounce
}));

Using Spies

Spies let you wrap existing functions while still allowing the original implementation to run. This is useful when you want to verify a function was called without replacing its behavior.

jest.spyOn() / vi.spyOn()

const math = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

test('spies on object methods', () => {
  const spy = jest.spyOn(math, 'add');
  
  math.add(2, 3);
  
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledWith(2, 3);
  expect(math.add(10, 5)).toBe(15); // Original still works
  
  spy.mockRestore(); // Restore original function
});

Spying on Module Methods

You can also spy on methods of modules you’ve imported:

const api = require('./api');
const axios = require('axios');

jest.mock('axios');

test('spies on axios get', () => {
  const spy = jest.spyOn(axios, 'get');
  axios.get.mockResolvedValue({ data: { name: 'Bob' } });
  
  api.getUser(1);
  
  expect(spy).toHaveBeenCalledWith('/users/1');
});

Mocking Timers

Code that uses setTimeout, setInterval, or setImmediate can be difficult to test. Jest and Vitest provide fake timers to control time in your tests.

useFakeTimers() / useFakeTimers()

// delayed.js
function notifyUser(callback) {
  setTimeout(() => {
    callback('User notified');
  }, 5000);
}
// delayed.test.js
function notifyUser(callback) {
  setTimeout(() => {
    callback('User notified');
  }, 5000);
}

test('notifies user after delay', () => {
  jest.useFakeTimers();
  const callback = jest.fn();
  
  notifyUser(callback);
  
  // Timer hasn't fired yet
  expect(callback).not.toHaveBeenCalled();
  
  // Fast-forward time
  jest.advanceTimersByTime(5000);
  
  // Now callback fired
  expect(callback).toHaveBeenCalledWith('User notified');
  
  jest.useRealTimers();
});

Timer Methods

MethodDescription
jest.runAllTimers()Run all pending timers
jest.advanceTimersByTime(ms)Move time forward by ms
jest.runOnlyPendingTimers()Run only pending timers

Modern Timer API (Vitest)

Vitest offers a cleaner timer API:

import { vi, fn } from 'vitest';

test('timers in Vitest', async () => {
  vi.useFakeTimers();
  
  const callback = vi.fn();
  notifyUser(callback);
  
  await vi.runTimersToTime(5000);
  
  expect(callback).toHaveBeenCalled();
  
  vi.useRealTimers();
});

Mocking Node.js Modules

Node.js has built-in modules like fs, path, and crypto that you often need to mock.

Mocking fs

jest.mock('fs');

const fs = require('fs');

test('reads file content', () => {
  fs.readFileSync.mockReturnValue('file contents');
  
  const content = fs.readFileSync('test.txt', 'utf8');
  
  expect(content).toBe('file contents');
  expect(fs.readFileSync).toHaveBeenCalledWith('test.txt', 'utf8');
});

Mocking Environment Variables

test('uses environment variable', () => {
  const originalEnv = process.env;
  process.env.API_KEY = 'test-key';
  
  // Your code here that uses process.env.API_KEY
  
  process.env = originalEnv; // Restore
});

Or use jest.spyOn for cleaner restoration:

test('spies on process.env', () => {
  const spy = jest.spyOn(process.env, 'API_KEY', 'get');
  spy.mockReturnValue('test-key');
  
  // Your test code
  
  spy.mockRestore();
});

Best Practices

  1. Mock at the appropriate level — Mock the closest dependency to your code, not everything.

  2. Keep mocks simple — Complex mocks are hard to maintain and can hide real problems.

  3. Verify mock interactions — Use matchers like toHaveBeenCalledWith to ensure your code calls dependencies correctly.

  4. Clean up after tests — Call mockRestore() or use jest.resetModules() when needed.

  5. Don’t mock everything — Sometimes it’s better to use the real implementation if it’s fast and deterministic.

  6. Use descriptive mock data — Mock return values should resemble real data structure.

Summary

You’ve learned the essential techniques for mocking in Jest and Vitest:

  • Creating mock functions with jest.fn() / vi.fn()
  • Mocking entire modules with jest.mock()
  • Using spies to wrap existing functions with jest.spyOn() / vi.spyOn()
  • Controlling time with fake timers
  • Mocking Node.js built-in modules like fs
  • Best practices for effective mocking

These techniques will help you write tests that are fast, reliable, and isolated from external dependencies. In the next tutorial, we’ll explore Testing DOM Code with jsdom to learn how to test browser-specific code.