AboutPostsToolsResumeContact

Mastering Mocking in Vitest: Beyond

Master Vitest mocking! Learn advanced techniques beyond vi.mock() for robust, reliable JavaScript/TypeScript tests. SpyOn, vi.fn, and more!

Photo de Sigmund sur Unsplash
Photo de Sigmund sur Unsplash

Introduction: The Why and When of Mocking

Setting the Stage: Why Mocking Matters

Imagine you're building a website for the Trade Federation, those folks from Star Wars. They're selling goods to both the Empire and the Rebels. Now, their website has a complex system for processing orders. It talks to a database, sends emails, and even checks stock levels with a remote API.

Here's the problem: when you want to test just one part of this system, say, the order processing logic, you don't want to rely on the database, the email service, or the external API. Why?

  • Slow Tests: Talking to real external services takes time. Your tests will be slow, and you'll be waiting a long time to see if your code works.
  • Unreliable Tests: If the database or API is down, your tests will fail, even if your code is perfect. This is called "flakiness," and it's a real headache.
  • Side Effects: Sending real emails or changing a real database can have unwanted side effects. You don't want to accidentally send a thousand emails to the Empire while testing!

That's where mocking comes in. Mocking means creating fake versions of these external dependencies. We replace the real database with a fake one, the real email service with a fake one, and so on. This isolates the part of your code you're testing, so you can focus on it without distractions.

Example:

// OrderProcessor.ts
import { Database } from './database';
import { EmailService } from './emailService';
import { StockAPI } from './stockAPI';

export class OrderProcessor {
  constructor(
    private database: Database,
    private emailService: EmailService,
    private stockAPI: StockAPI
  ) {}

  async processOrder(order: any) {
    await this.stockAPI.checkStock(order.productId);
    await this.database.saveOrder(order);
    await this.emailService.sendConfirmation(order.customerEmail);

    return 'Order processed';
  }
}

When testing OrderProcessor, we don't want to rely on real Database, EmailService, or StockAPI. We'll mock them!

The Pitfalls of Over-Mocking: Don't Lose Sight of Reality

Mocking is powerful, but it's easy to go overboard. If you mock everything, your tests might not reflect how your code works in the real world.

Imagine you mock the entire order processing system, including the business logic. Your tests might pass, but if there's a bug in the real logic, you won't catch it.

It's about finding the right balance. Mock external dependencies, but don't mock the core logic of the unit you're testing unless it is absolutely necessary.

Think of it this way: you want to test if the Trade Federation's order processor works correctly. But you don't want to test if the entire galaxy is working correctly.

Example:

If you mock the processOrder function itself, you will not test the logic inside of it, and the test is useless.

// OrderProcessor.test.ts
// Don't do this (unless you have a very good reason!)
vi.mock('./OrderProcessor', () => ({
  OrderProcessor: vi.fn().mockImplementation(() => ({
    processOrder: vi.fn().mockResolvedValue('Mocked order processed'),
  })),
}));

Introducing Fixtures: Setting Up a Consistent Stage

In the world of testing, a "fixture" is like a stage set. It's a way to create a consistent starting point for your tests.

Imagine you're testing the Trade Federation's inventory system. You need to create some sample products to test with. Instead of creating these products in every test, you can create a fixture.

Example: Object Fixture

// __fixtures__/product.fixtures.ts
export const productFixture = {
  productId: '123',
  name: 'Droid Parts',
  price: 100,
};

Then, in your test:

// discount.test.ts
import { expect, test } from 'vitest';
import { productFixture } from './__fixtures__/product.fixtures';
import { applyDiscount } from './discount';

test('should apply a discount to a product price', () => {
  // Arrange
  const discountedPrice = applyDiscount(productFixture.price, 20);

	// Assert
  expect(discountedPrice).toBe(80);
});

Example: Function Fixture

You can also use functions to create more complex fixtures:

// __fixtures__/order.fixtures.ts
export const createOrderFixture = (productId: string, quantity: number) => ({
  orderId: 'order-66',
  productId,
  quantity,
  customerEmail: 'rebel@alliance.com',
});

And in your test:

// order.test.ts
import { expect, test } from 'vitest';
import { createOrderFixture } from './__fixtures__/order.fixtures.ts';
import { productFixture } from './__fixtures__/product.fixtures.ts';
import { calculateTotal } from './order.ts';

test('should calculate the total price of an order', () => {
  // Arrange
  const order = createOrderFixture('123', 5);
  const total = calculateTotal(order.quantity, productFixture.price);

	// Assert
  expect(total).toBe(500);
});

Using fixtures makes your tests:

  • DRY (Don't Repeat Yourself): You avoid repeating the same setup code.
  • Readable: Your tests are cleaner and easier to understand.
  • Maintainable: If you need to change the setup, you only change it in one place.

Core Mocking Techniques: A Deep Dive

`vi.spyOn()`: Being a Code Detective (and Letting the Suspect Act)

Imagine you need to keep tabs on a function, but you don't want to stop it from doing its job. That's the beauty of vi.spyOn(). It's like placing a wiretap—you're listening in, but the conversation keeps going. You can see what a function is doing, how many times it's called, and what arguments it receives, all while letting it execute its actual logic.

Let's say the Trade Federation has a ShipmentProcessor class:

// shipment.ts
export class ShipmentProcessor {
  sendShipment(orderId: string, address: string) {
    console.log(`Sending shipment ${orderId} to ${address}`);
    // ... some real shipping logic ...
    return `Shipment ${orderId} sent`;
  }
}

Here's how you'd spy on the sendShipment method:

// shipment.test.ts
import { vi, expect, test } from 'vitest';
import { ShipmentProcessor } from './shipment';

test('should call sendShipment with correct arguments and return the correct value', () => {
  // Arrange
  const processor = new ShipmentProcessor();
  const spy = vi.spyOn(processor, 'sendShipment');

  const orderId = 'order-66';
  const address = 'Coruscant';

  // Act
  const result = processor.sendShipment(orderId, address);

  // Assert
  expect(spy).toHaveBeenCalledWith(orderId, address);
  expect(spy).toHaveBeenCalledTimes(1);
  expect(result).toBe(`Shipment ${orderId} sent`); // Check the returned value

  spy.mockRestore(); // Clean up the spy!
});

Notice how we're also checking the returned value of sendShipment. This proves that the actual function logic is executed. Remember, vi.spyOn() lets the function run its course while you observe.

**`vi.fn()`****: Building Your Own Fake Functions (With Care)**

Now, sometimes you need to completely replace a function with a fake one. That's where vi.fn() comes in. It's like building a robot that mimics a real person. You get to decide exactly what the function returns, what errors it throws, or what logic it runs.

But here's the catch: you need to be careful. If your fake function doesn't behave similarly to the real one, your tests might not be relevant. You need to mimic the real function's behavior to some extent.

Let's look at an example with a database:

// database.ts
export class Database {
  async saveOrder(data: any) {
    // ... real database logic ...
    return 'Data saved';
  }
}
// OrderProcessor.ts
import { Database } from './database';
import { EmailService } from './emailService';
import { StockAPI } from './stockAPI';

export class OrderProcessor {
  constructor(
    private database: Database,
    private emailService: EmailService,
    private stockAPI: StockAPI
  ) {}

  async processOrder(order: any) {
    await this.stockAPI.checkStock(order.productId);
    await this.database.saveOrder(order);
    await this.emailService.sendConfirmation(order.customerEmail);

    return 'Order processed';
  }
}
// orderProcessor.test.ts
import { createOrderFixture } from './__fixtures__/order.fixtures';
import { OrderProcessor } from './orderProcessor';
import { vi, expect, test } from 'vitest';

test('should save order to database', async () => {
  // Arrange
  const mockDatabase = { saveOrder: vi.fn() };
  const mockEmailService = { sendConfirmation: vi.fn() };
  const mockStockAPI = { checkStock: vi.fn() };

  const processor = new OrderProcessor(mockDatabase, mockEmailService, mockStockAPI);

  const order = createOrderFixture('123', 5);

  // Act
  await processor.processOrder(order);

  // Assert
  expect(mockDatabase.saveOrder).toHaveBeenCalledWith(order);
});

test('should use mockImplementationOnce to control the return value of a function for a single call', () => {
  // Arrange
  const mockFunction = vi.fn();
  mockFunction.mockImplementationOnce(()=> 'first call');
  mockFunction.mockImplementationOnce(()=> 'second call');

  // Act / Assert
  expect(mockFunction()).toBe('first call');
  expect(mockFunction()).toBe('second call');
  expect(mockFunction()).toBe(undefined);
})

test('should use mockReturnValueOnce to control the return value of a function for a single call', () => {
  // Arrange
  const mockFunction = vi.fn();
  mockFunction.mockReturnValueOnce('first call');
  mockFunction.mockReturnValueOnce('second call');

  // Act / Assert
  expect(mockFunction()).toBe('first call');
  expect(mockFunction()).toBe('second call');
  expect(mockFunction()).toBe(undefined);
})

**Manual Mocks: Taking Full Control (with** **`vi.fn()`****)**

When vi.mock() and vi.fn() aren't enough, you need to create a completely custom mock for a module. This is where manual mocks come in, and you can even use vi.fn() within them.

Let's say you have a module that reads data from a file:

// fileReader.ts
import * as fs from 'fs';

export function readData(filePath: string) {
  return fs.readFileSync(filePath, 'utf-8');
}

Create a __mocks__ directory and a fileReader.ts file inside:

// __mocks__/fileReader.ts
import { vi } from 'vitest';

export const readData = vi.fn((filePath: string) => {
  if (filePath === 'test.txt') {
    return 'Mocked data';
  }
  return '';
});

Now, in your test:

// fileReader.test.ts
import { readData } from './fileReader';
import { vi, expect, test } from 'vitest';

// This line will automatically replace every import of 'fileReader' with the mocked version
vi.mock('./fileReader');

test('should use mocked file data', () => {
  // Arrange / Act
  const data = readData('test.txt');

  // Assert
  expect(data).toBe('Mocked data');
  expect(readData).toHaveBeenCalledWith('test.txt');
  expect(readData).toHaveBeenCalledTimes(1);
});

Using vi.fn() in your manual mock gives you the ability to check call counts and arguments, adding even more control and precision to your tests.

Advanced Mocking Scenarios

Mocking Asynchronous Functions and Promises: Taming the Async Beast

In the world of JavaScript, asynchronous code is everywhere. Dealing with promises and async/await in tests can be tricky, but Vitest provides powerful tools to make it easier.

Mocking with `mockResolvedValue()` and `mockRejectedValue()`

Imagine you have a function that fetches data from an API:

// apiService.ts
export class ApiService {
  async fetchData(url: string) {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  }
}

Here's how you'd mock it:

// apiService.test.ts
import { ApiService } from './apiService';
import { vi, expect, test } from 'vitest';

test('should resolve with data', async () => {
  // Arrange
  const apiService = new ApiService();
  const fetchDataSpy = vi.spyOn(apiService, 'fetchData');
  fetchDataSpy.mockResolvedValue({ message: 'Data received' });

  // Act
  const data = await apiService.fetchData('https://example.com/data');

  // Assert
  expect(data).toEqual({ message: 'Data received' });
  expect(fetchDataSpy).toHaveBeenCalledWith('https://example.com/data');
});

test('should reject with an error', async () => {
  // Arrange
  const apiService = new ApiService();
  const fetchDataSpy = vi.spyOn(apiService, 'fetchData');
  fetchDataSpy.mockRejectedValue(new Error('Network error'));

  // Act / Assert
  await expect(apiService.fetchData('https://example.com/data')).rejects.toThrow('Network error');
  expect(fetchDataSpy).toHaveBeenCalledWith('https://example.com/data');
});
Complex Asynchronous Mocks with `mockImplementation(async () => {...})`

For more complex scenarios, you can use mockImplementation with an async function:

test('should handle complex async logic', async () => {
  // Arrange
  const apiService = new ApiService();
  const mockFetchData = vi.spyOn(apiService, 'fetchData').mockImplementation(async (url: string) => {
    if (url === 'https://example.com/special') {
      return { special: true };
    }
    return { normal: true };
  });

  // Act
  const specialData = await apiService.fetchData('https://example.com/special');
  const normalData = await apiService.fetchData('https://example.com/normal');

  // Assert
  expect(specialData).toEqual({ special: true });
  expect(normalData).toEqual({ normal: true });
  mockFetchData.mockRestore();
});

Mocking External APIs: Building a Fake Galaxy

When your code interacts with external APIs, you need to mock those calls to keep your tests fast and reliable.

Using `fetch` Mocks

As shown in the previous examples, mocking fetch is a common way to handle API calls. You can control the responses and simulate different scenarios.

Using Mock Service Worker (MSW)

For more complex API mocking, consider using MSW. It lets you intercept network requests at the network level, providing a realistic mocking experience.

// __msw__/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/products', () => {
    return HttpResponse.json([{ id: 1, name: 'Droid Parts' }]);
  }),
  http.post('https://api.example.com/orders', () => {
    return HttpResponse.json({ orderId: 'order-66' });
  }),
];
// api.test.ts
import { setupServer } from 'msw/node';
import { afterAll, afterEach, beforeAll, expect, test } from 'vitest';
import { handlers } from './__msw__/handlers';
import { fetchProducts, createOrder } from './api.ts';

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('should fetch products', async () => {
  const products = await fetchProducts();
  expect(products).toEqual([{ id: 1, name: 'Droid Parts' }]);
});

test('should create an order', async () => {
  const order = await createOrder({ productId: 1, quantity: 2 });
  expect(order).toEqual({ orderId: 'order-66' });
});

Mocking Browser APIs in Node.js: Bringing the Browser to Node

Sometimes, your code might run in a Node.js environment but still rely on browser-specific APIs like window or localStorage. This is common in frameworks that support server-side rendering or when you're using libraries that make use of browser globals. In these cases, you need to mock those APIs to ensure your tests run correctly in Node.js.

Using `vi.stubGlobal()`

vi.stubGlobal() lets you replace global objects with mock values. This is useful for simple mocks. Let's create a Star Wars example:

// planetStorage.ts
export function saveLastVisitedPlanet(planet: string) {
  localStorage.setItem('lastVisitedPlanet', planet);
}

export function getLastVisitedPlanet() {
  return localStorage.getItem('lastVisitedPlanet');
}
// planetStorage.test.ts
import { saveLastVisitedPlanet, getLastVisitedPlanet } from './planetStorage';
import { vi, expect, test } from 'vitest';

test('should mock localStorage for planet storage', () => {
  // Arrange
  const mockLocalStorage = {
    store: {},
    setItem: vi.fn((key, value) => (mockLocalStorage.store[key] = value)),
    getItem: vi.fn((key) => mockLocalStorage.store[key]),
  };

  vi.stubGlobal('localStorage', mockLocalStorage);

  // Act
  saveLastVisitedPlanet('Tatooine');
  const lastPlanet = getLastVisitedPlanet();

  // Assert
  expect(mockLocalStorage.setItem).toHaveBeenCalledWith('lastVisitedPlanet', 'Tatooine');
  expect(lastPlanet).toBe('Tatooine');
});

Using `jsdom`

For more complex browser environments, use jsdom. It creates a simulated browser environment in Node.js, allowing you to test code that relies on the document object or other browser APIs.

// documentUtil.ts
export function getTitle() {
  return document.title;
}
// documentUtil.test.ts
import { expect, test, beforeAll } from 'vitest';
import { JSDOM } from 'jsdom';
import { getTitle } from './documentUtil';

beforeAll(() => {
  const dom = new JSDOM('<!DOCTYPE html><title>Trade Federation Page</title>');
  global.document = dom.window.document;
});

test('should mock document.title', () => {
  expect(getTitle()).toBe('Trade Federation Page');
});

Mocking Classes: Shaping the Object World

Mocking classes is essential when your code depends on complex objects. It allows you to isolate the unit under test and control the behavior of its dependencies.

Mocking Class Methods

Use vi.spyOn() or vi.fn() to mock class methods. This is useful when you want to observe or modify the behavior of specific methods.

// droid.ts
export class Droid {
  speak() {
    return 'Beep boop';
  }
}
// droid.test.ts
import { Droid } from './droid';
import { vi, expect, test } from 'vitest';

test('should mock droid speak', () => {
  const droid = new Droid();
  const speakSpy = vi.spyOn(droid, 'speak').mockReturnValue('Mocked beep');
  expect(droid.speak()).toBe('Mocked beep');
  expect(speakSpy).toHaveBeenCalled();
});
Mocking Class Constructors

Use vi.mock() to mock class constructors. This is useful when you want to replace the entire class with a mock implementation.

// communicator.ts
import { Droid } from "./droid";

export class Communicator {
  constructor(private droid: Droid) {}
  communicate() {
    return this.droid.speak();
  }
}
// communicator.test.ts
import { Communicator } from './communicator';
import { Droid } from './droid';
import { vi, expect, test, Mock } from 'vitest';

vi.mock('./droid');

test('should mock droid constructor', () => {
  vi.mocked(Droid).mockImplementation(() => ({
    speak: vi.fn().mockReturnValue('Mocked constructor beep'),
  }));

  const communicator = new Communicator(new Droid());
  expect(communicator.communicate()).toBe('Mocked constructor beep');
});

Verification and Assertion Techniques: Proving Your Tests Right

Mocking is only half the battle. Once you've set up your mocks, you need to verify that they're being used correctly. This is where Vitest's assertion techniques come in. Let's dive into how to use them effectively.

`toHaveBeenCalledTimes()`: Ensuring Function Calls and Catching Unexpected Behavior

Verifying that a function was called, and how many times, is crucial. It ensures that your code is executing the expected logic. While toHaveBeenCalled() simply checks if a function was called at least once, toHaveBeenCalledTimes() provides a more precise verification.

Imagine you have a DroidCommunicator that sends messages:

// droidCommunicator.ts
export class DroidCommunicator {
  sendMessage(message: string, droidId: string) {
    console.log(`Sending message "${message}" to droid ${droidId}`);
    // ... actual communication logic ...
  }
}

Here's how you'd verify the function calls:

// droidCommunicator.test.ts
import { DroidCommunicator } from './droidCommunicator';
import { vi, expect, test } from 'vitest';

test('should call sendMessage exactly once', () => {
  // Arrange
  const communicator = new DroidCommunicator();
  const sendMessageSpy = vi.spyOn(communicator, 'sendMessage');

  // Act
  communicator.sendMessage('Hello there!', 'R2-D2');

  // Assert
  expect(sendMessageSpy).toHaveBeenCalledTimes(1);
});

test('should call sendMessage multiple times', () => {
  // Arrange
  const communicator = new DroidCommunicator();
  const sendMessageSpy = vi.spyOn(communicator, 'sendMessage');

  // Act
  communicator.sendMessage('Message 1', 'C-3PO');
  communicator.sendMessage('Message 2', 'C-3PO');

  // Assert
  expect(sendMessageSpy).toHaveBeenCalledTimes(2);
});

Now, let's explore a situation where toHaveBeenCalledTimes() helps you catch unexpected behavior. Suppose your DroidCommunicator has a feature where it should only send a message once, even if the user tries to send it multiple times in quick succession:

// droidCommunicator.ts
export class DroidCommunicator {
  private messageSent = false;

  constructor(private logger: (text: string) => void) {}

  sendMessage(message: string, droidId: string) {
    if (!this.messageSent) {
      this.logger(`Sending message "${message}" to droid ${droidId}`);
      // ... actual communication logic ...
      this.messageSent = true;
    }
  }
}
test('should only send message once, even if called multiple times', () => {
  // Arrange
  const logger = vi.fn();
  const communicator = new DroidCommunicator(logger);

  // Act
  communicator.sendMessage('Urgent Message', 'BB-8');
  communicator.sendMessage('Urgent Message', 'BB-8');
  communicator.sendMessage('Urgent Message', 'BB-8');

  // Assert
  expect(logger).toHaveBeenCalledTimes(1);
});

In this example, toHaveBeenCalledTimes(1) ensures that the message is only sent once, even though sendMessage is called multiple times. This helps catch potential bugs and improves the robustness of your code by preventing unintended side effects.

`toHaveBeenCalledWith()` and `toHaveBeenLastCalledWith()`: Checking Arguments

Verifying the arguments passed to mock functions is essential to ensure that your code is passing the correct data. toHaveBeenCalledWith() checks if the mocked function was called with the specific arguments at least once, while toHaveBeenLastCalledWith() checks the arguments of the last call.

test('should call sendMessage with specific arguments', () => {
  // Arrange
  const communicator = new DroidCommunicator(console.log);
  const sendMessageSpy = vi.spyOn(communicator, 'sendMessage');

  // Act
  communicator.sendMessage('May the Force be with you', 'BB-8');

  // Assert
  expect(sendMessageSpy).toHaveBeenCalledWith('May the Force be with you', 'BB-8');
});

test('should check the last call arguments', () => {
    // Arrange
    const communicator = new DroidCommunicator(console.log);
    const sendMessageSpy = vi.spyOn(communicator, 'sendMessage');

    // Act
    communicator.sendMessage('Message 1', 'C-3PO');
    communicator.sendMessage('Message 2', 'R2-D2');

    // Assert
    expect(sendMessageSpy).toHaveBeenLastCalledWith('Message 2', 'R2-D2');
});
Why Check Arguments?

Checking arguments is crucial because it ensures that your mocks are being called with the correct data. Without this, your tests might pass, but your code might still have bugs. For example, if you're mocking an API call, you need to make sure that the correct parameters are being sent to the API.

Custom Matchers (Optional): Extending Vitest's Assertions

For more complex assertions, you can create custom matchers. This is useful when you have specific assertion logic that you want to reuse across multiple tests.

Here's a simple example:

// __helpers__/customMatchers.ts
import { expect } from 'vitest';

expect.extend({
  toBeDroidId(received, expected) {
    const pass = received.startsWith('C');
    if (pass) {
      return {
        message: () => `expected ${received} not to be a droid ID`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be a droid ID`,
        pass: false,
      };
    }
  },
});
// vitest.d.ts
import 'vitest';

interface CustomMatchers<R = unknown> {
  toBeDroidId: () => R
}

declare module 'vitest' {
  interface Assertion<T = any> extends CustomMatchers<T> {}
  interface AsymmetricMatchersContaining extends CustomMatchers {}
}
// droidCommunicator.test.ts
import './customMatchers'; // Import the custom matchers
import { expect, test } from 'vitest';

test('should use custom matcher', () => {
  expect('D123').toBeDroidId();
  expect('C3PO').not.toBeDroidId();
});

Explanation:

  • toHaveBeenCalledTimes(number): Verifies that a mock function has been called a specific number of times, allowing you to catch unexpected behavior.
  • toHaveBeenCalledWith(...args): Verifies that a mock function has been called with specific arguments.
  • toHaveBeenLastCalledWith(...args): Verifies that the last call to a mock function was with specific arguments.
  • Custom matchers: Allow you to define your own assertion logic for more complex scenarios.

Best Practices and Conclusion: Crafting Maintainable and Reliable Tests

Keep Mocks Focused: Mock Only What You Must

One of the most common pitfalls in testing is over-mocking. It's tempting to mock everything, but this can lead to tests that are disconnected from the actual behavior of your code. Remember, the goal of mocking is to isolate the unit under test, not to replace every dependency.

Only mock external dependencies or parts of your code that are difficult to test directly. For example, mock API calls, database interactions, or complex third-party libraries. But avoid mocking the core logic of the unit you're testing.

Ask yourself, "What am I trying to test?" If the answer involves the interaction with an external system or a complex dependency, then mocking is appropriate. Otherwise, try to test the code as it is.

Maintain Test Readability: Clarity is Key

Tests should be as readable as possible. This makes them easier to understand, maintain, and debug. Here are a few tips:

  • Use descriptive mock names: Instead of mockFn, use names like mockApiService or mockDatabaseSave.
  • Add comments: Explain why you're mocking a particular dependency or what a specific assertion is checking.
  • Keep tests concise: Break down complex tests into smaller, more focused ones.
  • Follow the Arrange-Act-Assert pattern: This makes your tests easier to follow.

Summary: Empowering Your Testing Skills

In this guide, we've explored advanced mocking techniques in Vitest, going beyond the basics to tackle complex scenarios. We've covered:

  • The purpose and benefits of mocking.
  • Core mocking techniques like vi.spyOn(), vi.fn(), and manual mocks.
  • Advanced mocking scenarios, including asynchronous functions, external APIs, browser APIs, and classes.
  • Verification and assertion techniques to ensure your mocks are used correctly.
  • Fixtures to keep your tests DRY.

By mastering these techniques, you can write more robust, reliable, and maintainable tests. Remember to mock only what's necessary, keep your tests readable, and always verify your mocks with appropriate assertions.

Further Resources:

I encourage you to apply these techniques in your own projects and continue to explore the power of Vitest. Happy testing!

Vitest Mocking Cheatsheet

Core Mocks

  • vi.spyOn(obj, method): Spy on a method, keep original.
  • vi.fn(impl?): Create a mock function.
  • vi.mock(path, factory?): Mock a module.
  • __mocks__/module.ts: Manual mock.

Mock Behavior

  • mockFn.mockImplementationOnce(fn): Set mock function behavior once.
  • mockFn.mockReturnValueOnce(val): Set return value once.
  • mockFn.mockResolvedValueOnce(val): Set resolved promise value once.
  • mockFn.mockRejectValueOnce(err): Set rejected promise value.
  • spy.mockRestore(): Restore original method.

Assertions

  • expect(mock).toHaveBeenCalledTimes(n): Check call count.
  • expect(mock).toHaveBeenCalledWith(...args): Check call args.
  • expect(mock).toHaveBeenLastCalledWith(...args): Check last call args.

Browser/API

  • vi.stubGlobal(name, val): Mock global object.
  • jsdom: Browser env in Node.
  • MSW: API mocking (setupServer, rest.get/post).
You liked the post? Consider donating!
Become a patron
Buy me a coffee