rotvalli.dev

Mocking Functions and Classes with Vitest

In this tutorial, we'll explore testing Typescript code using Vitest. We'll learn how to mock functions and classes. You can find the code for this tutorial on GitHub.

Creating a Simple App

Setting Up

  1. Create a new npm project with npm init and install TypeScript and Vitest:
npm install -D typescript vitest
  1. Set up a basic tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true
  },
  "include": ["src/"]
}
  1. Configure Vitest in vitest.config.mts:
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {},
});

Testing configuration

  1. Create a file src/sum.ts:
/** src/sum.ts */
export const sum = (a: number, b: number): number => a + b;
  1. Write a test for the sum function in src/sum.test.ts:
/** src/sum.test.ts */
import { expect, test } from "vitest";
import { sum } from "./sum";

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});
  1. Update your project's test script to use Vitest:
// package.json
{
  "scripts": {
    "test": "vitest --run"
  }
}
  1. Run the tests with npm run test. You should see the test output.

Sum test output

Using .toMatchInlineSnapshot() for Assertions

Let's improve our assertions by using .toMatchInlineSnapshot(). Modify the test as follows:

/** src/sum.test.ts */
import { expect, test } from "vitest";
import { sum } from "./sum";

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toMatchInlineSnapshot();
});

Running the test will update the snapshot in the code. You can read more about snapshots in the official Vitest documentation. Run the tests again with npm run test and verify that the snapshot is updated.

Adding functions

Now, let's create a function that fetches a person's name and address using fetchPersonName and fetchPersonAddress functions.

  1. Create a src/fetch.ts file:
/** src/fetch.ts */
export const fetchPersonName = async (id: string): Promise<string> => {
  return `Name ${id}`;
};

export const fetchPersonAddress = async (id: string): Promise<string> => {
  return `Address ${id}`;
};
  1. Next, create a src/person.ts file:
/** src/person.ts */
import { fetchPersonAddress, fetchPersonName } from "./fetch";

type Person = {
  id: string;
  name: string;
  address: string;
};

export const getPerson = async (id: string): Promise<Person> => {
  return {
    id,
    name: await fetchPersonName(id),
    address: await fetchPersonAddress(id),
  };
};

Mocking functions

In this section, we'll explore how to mock functions using Vitest. We'll be testing the getPerson function and try different ways to mock fetchPersonName function.

Basic Test without Mocking

First, let's create a basic test for the getPerson function. We'll call it with an id value of 1 and assert its result using a snapshot:

/** src/person.test.ts */
import { describe, expect, test } from "vitest";
import { getPerson } from "./person";

describe("person tests", () => {
  test("without mock", () => {
    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Name 1",
      }
    `);
  });
});

Run the test, and it should pass:

Person test output

Mocking with vi.spyOn

Next, let's mock the fetchPersonName function using vi.spyOn. Start by importing the entire fetch module with import * as fetchModule from "./fetch";. Then spy on the module and the fetchPersonName function, providing a one-time mock implementation with mockImplementationOnce:

import { describe, expect, test, vi } from "vitest";
import { getPerson } from "./person";
import * as fetchModule from "./fetch";

describe("person tests", () => {
  test("without mock", () => {
    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Name 1",
      }
    `);
  });
});

describe("person tests with vi.spyOn", () => {
  test("mocked with vi.spyOn", () => {
    vi.spyOn(fetchModule, "fetchPersonName").mockImplementationOnce(
      async () => "Mocked name with vi.spyOn",
    );
    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Mocked name with vi.spyOn",
      }
    `);
  });
});

Running tests should pass:

Person test output vi.spyOn

Mocking with vi.mock and vi.fn:

Finally, let's mock fetchPersonName using vi.mock and vi.fn. We'll mock the entire fetch module, preserving all original implementations except for fetchPersonName:

describe("person tests with vi.mock and vi.fn", () => {
  test("mocked with vi.mock", () => {
    vi.mock("./fetch", async (importOriginal) => ({
      ...(await importOriginal<typeof import("./fetch")>()),
      fetchPersonName: vi.fn(),
    }));

    vi.mocked(fetchModule.fetchPersonName).mockReturnValueOnce(
      Promise.resolve("Mocked with vi.fn"),
    );

    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Mocked with vi.fn",
      }
    `);
  });
});

Run the tests, and it should give you a snapshot error:

Person test output vi.mock error

This happens because vi.mock: Mocks every import call to the module even if it was already statically imported. Let's comment out the without mock test. The final code should look like this:

/** src/person.test.ts */
import { describe, expect, test, vi } from "vitest";
import { getPerson } from "./person";
import * as fetchModule from "./fetch";

/* describe("person tests", () => {
  test("without mock", () => {
    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
    {
      "address": "Address 1",
      "id": "1",
      "name": "Name 1",
    }
  `);
  });
}); */

describe("person tests with vi.spyOn", () => {
  test("mocked with vi.spyOn", () => {
    vi.spyOn(fetchModule, "fetchPersonName").mockImplementationOnce(
      async () => "Mocked name with vi.spyOn",
    );
    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Mocked name with vi.spyOn",
      }
    `);
  });
});

describe("person tests with vi.mock and vi.fn", () => {
  test("mocked with vi.mock", () => {
    vi.mock("./fetch", async (importOriginal) => ({
      ...(await importOriginal<typeof import("./fetch")>()),
      fetchPersonName: vi.fn(),
    }));

    vi.mocked(fetchModule.fetchPersonName).mockReturnValueOnce(
      Promise.resolve("Mocked with vi.fn"),
    );

    expect(getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Address 1",
        "id": "1",
        "name": "Mocked with vi.fn",
      }
    `);
  });
});

Run the tests, and they should pass:

Person test output vi.mock

Mocking Classes

Let's refactor our getPerson function into a class:

/** src/person-service.ts */
import { fetchPersonAddress, fetchPersonName } from "./fetch";

type Person = {
  id: string;
  name: string;
  address: string;
};

export class PersonService {
  async getPerson(id: string): Promise<Person> {
    return {
      id,
      name: await fetchPersonName(id),
      address: await fetchPersonAddress(id),
    };
  }
}

Create a test file for the class. Then spy on the class prototype and mock the implementation:

/** src/person-service.test.ts */
import { describe, expect, test, vi } from "vitest";
import { PersonService } from "./person-service";

describe("person service tests", () => {
  test("mocked with vi.mock", () => {
    vi.spyOn(PersonService.prototype, "getPerson").mockImplementationOnce(
      async (id) => ({
        id,
        name: "Mocked name",
        address: "Mocked address",
      }),
    );

    const service = new PersonService();

    expect(service.getPerson("1")).resolves.toMatchInlineSnapshot(`
      {
        "address": "Mocked address",
        "id": "1",
        "name": "Mocked name",
      }
    `);
  });
});

That's it! Now run the tests, and they should pass:

Person service test output

Conclusion

In this tutorial, we've delved into various techniques for mocking functions and classes using Vitest and TypeScript.