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
- Create a new npm project with
npm init
and install TypeScript and Vitest:
npm install -D typescript vitest
- Set up a basic
tsconfig.json
:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
},
"include": ["src/"]
}
- Configure Vitest in
vitest.config.mts
:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {},
});
Testing configuration
- Create a file
src/sum.ts
:
/** src/sum.ts */
export const sum = (a: number, b: number): number => a + b;
- Write a test for the
sum
function insrc/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);
});
- Update your project's test script to use Vitest:
// package.json
{
"scripts": {
"test": "vitest --run"
}
}
- Run the tests with
npm run test
. You should see the 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.
- 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}`;
};
- 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:
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:
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:
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:
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:
Conclusion
In this tutorial, we've delved into various techniques for mocking functions and classes using Vitest and TypeScript.