Course:Node.js & Express/
Lesson

Unit tests are supposed to be fast and isolated. But what happens when the function you want to test calls a database, sends an email, or makes a request to an external APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.? You cannot let those side effects run in tests, they are slow, unpredictable, and expensive. This is where mocks and spies come in.

Why you need mocks

Think of a mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions. like a stunt double in a film. The stunt double looks like the actor, responds to the director's cues, and lets the scene get filmed safely, but nothing real is actually at risk. A mock database looks like the real thing to your code, returns whatever you tell it to, and never touches an actual server.

Mocking serves two goals: isolation (your test only tests your code, not the dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself.'s code) and control (you decide exactly what the dependency returns, including errors and edge cases).

When to mock vs. use the real thing

Use a mock when...Use the real thing when...
The dependency is slow (DB, network)You are writing an integration test
You need to simulate errorsThe dependency is lightweight (pure function)
The dependency has external costsYou want to verify the wiring is correct
You want to test without setupYou have a test database available
02

Creating mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions. functions with vi.fn()

vi.fn() creates a mock function, a fake function that records every call made to it.

import { vi, describe, it, expect } from 'vitest';

describe('sendWelcomeEmail', () => {
  it('calls the email service with correct arguments', async () => {
    // Create a mock email service
    const mockEmailService = {
      send: vi.fn().mockResolvedValue({ messageId: 'abc-123' })
    };

    const user = { name: 'Alice', email: 'alice@example.com' };
    await sendWelcomeEmail(user, mockEmailService);

    // Verify it was called correctly
    expect(mockEmailService.send).toHaveBeenCalledTimes(1);
    expect(mockEmailService.send).toHaveBeenCalledWith({
      to: 'alice@example.com',
      subject: 'Welcome to the platform',
      body: expect.stringContaining('Alice')
    });
  });
});

Controlling return values

You can make a mock return different values depending on when it is called:

const mockFn = vi.fn()
  .mockReturnValue('default')        // All calls return 'default'
  .mockReturnValueOnce('first call') // First call returns this
  .mockReturnValueOnce('second call'); // Second call returns this

console.log(mockFn()); // 'first call'
console.log(mockFn()); // 'second call'
console.log(mockFn()); // 'default'
console.log(mockFn()); // 'default'

For async functions, use mockResolvedValue and mockRejectedValue:

const mockDb = {
  findUser: vi.fn()
    .mockResolvedValueOnce({ id: 1, name: 'Alice' }) // succeeds
    .mockRejectedValueOnce(new Error('Connection lost')) // then fails
};
03

Mocking entire modules with vi.mock()

Sometimes you need to replace every function exported from a moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.. vi.mock() does this at the module level, any file that imports the mocked module gets the fake version.

// vi.mock() must be at the top level - Vitest hoists it automatically
vi.mock('../database/users.js', () => ({
  findById: vi.fn(),
  create: vi.fn(),
  deleteById: vi.fn()
}));

import { findById, create } from '../database/users.js';

describe('UserService.create', () => {
  afterEach(() => {
    vi.clearAllMocks(); // Reset call counts and return values
  });

  it('creates a user and returns it', async () => {
    create.mockResolvedValue({ id: 42, name: 'Bob' });

    const result = await UserService.create({ name: 'Bob' });

    expect(create).toHaveBeenCalledWith({ name: 'Bob' });
    expect(result.id).toBe(42);
  });
});
vi.mock() is hoisted to the top of the file by Vitest, even if you write it in the middle of your code. This is why the import that comes after it still gets the mocked version, the mock is always set up first.
04

Spying on existing functions

A spyWhat is spy?A test wrapper around a real function that records how it was called (arguments, return values) without replacing its implementation. is different from a mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions.. A mock replaces a function entirely. A spy wraps a real function so you can observe it, by default, the real implementation still runs.

import * as emailModule from '../services/email.js';

describe('UserService', () => {
  it('calls sendEmail when user is created', async () => {
    // Spy on the real function
    const emailSpy = vi.spyOn(emailModule, 'sendEmail');

    await UserService.create({ name: 'Charlie', email: 'c@example.com' });

    expect(emailSpy).toHaveBeenCalledTimes(1);

    // Restore original implementation after
    emailSpy.mockRestore();
  });
});

You can also override the implementation temporarily:

const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Now console.error is silenced for this test

Common spy matchers

expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
expect(spy).toHaveBeenLastCalledWith('last-arg');
expect(spy).toHaveReturnedWith('value');
05

Mocking timers and dates

Tests that depend on Date.now(), setTimeout, or setInterval are fragile because they depend on real time. Vitest lets you fake the clock:

describe('rate limiter', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('resets after one minute', async () => {
    const limiter = new RateLimiter({ maxRequests: 5, windowMs: 60_000 });

    // Use up all requests
    for (let i = 0; i < 5; i++) {
      limiter.check('user-1');
    }

    expect(() => limiter.check('user-1')).toThrow('Rate limit exceeded');

    // Advance time by 60 seconds
    vi.advanceTimersByTime(60_000);

    // Should work again
    expect(() => limiter.check('user-1')).not.toThrow();
  });
});
06

Quick reference

APIWhat it does
vi.fn()Create an empty mock function
vi.fn().mockReturnValue(v)Mock always returns v
vi.fn().mockResolvedValue(v)Mock always resolves to v
vi.fn().mockRejectedValue(e)Mock always rejects with e
vi.fn().mockImplementation(fn)Mock uses custom implementation
vi.mock('module', factory)Replace an entire module
vi.spyOn(obj, 'method')Spy on an existing method
vi.clearAllMocks()Reset call history (keep implementation)
vi.resetAllMocks()Reset call history + return values
vi.restoreAllMocks()Restore all spies to originals
vi.useFakeTimers()Take control of the clock
javascript
// Mocking example
import { vi, describe, it, expect } from 'vitest';

// Mock a module
vi.mock('./database', () => ({
  query: vi.fn().mockResolvedValue({ rows: [] })
}));

// Mock function
const mockFn = vi.fn()
  .mockReturnValue('default')
  .mockReturnValueOnce('first')
  .mockReturnValueOnce('second');

// Spy on existing function
const spy = vi.spyOn(console, 'log');

// Verify calls
expect(mockFn).toHaveBeenCalledWith('arg');
expect(mockFn).toHaveBeenCalledTimes(2);