Basics of Vitest

What is Vitest?

Vitest is a JavaScript/TypeScript testing framework.

It is commonly used with Vite projects and provides:

Installation

pnpm add -D vitest

Add a test script to package.json:

{
  "scripts": {
    "test": "vitest"
  }
}

Run tests:

pnpm test

Run once:

pnpm vitest run

Basic Test

Create math.test.ts:

import { describe, expect, it } from "vitest";

function add(a: number, b: number) {
  return a + b;
}

describe("add", () => {
  it("adds two numbers", () => {
    expect(add(2, 3)).toBe(5);
  });
});

Run:

pnpm vitest

it and test

They are equivalent:

it("adds numbers", () => {
  expect(1 + 1).toBe(2);
});
test("adds numbers", () => {
  expect(1 + 1).toBe(2);
});

describe

Groups related tests:

describe("math", () => {
  it("adds", () => {
    expect(1 + 2).toBe(3);
  });

  it("multiplies", () => {
    expect(2 * 3).toBe(6);
  });
});

Assertions

Vitest uses expect().

Equality

expect(2 + 2).toBe(4);
expect({ name: "Marvin" }).toEqual({ name: "Marvin" });

Truthiness

expect(true).toBeTruthy();
expect(false).toBeFalsy();

Null / undefined

expect(null).toBeNull();
expect(undefined).toBeUndefined();

Numbers

expect(10).toBeGreaterThan(5);
expect(10).toBeLessThan(20);

Strings

expect("hello world").toContain("world");

Arrays

expect([1, 2, 3]).toContain(2);

Testing Errors

function divide(a: number, b: number) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }

  return a / b;
}

Test it:

it("throws when dividing by zero", () => {
  expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
});

Async Tests

Vitest supports promises:

it("gets a user", async () => {
  const user = await getUser();

  expect(user.name).toBe("Marvin");
});

You can also return the promise:

it("gets a user", () => {
  return getUser().then((user) => {
    expect(user.name).toBe("Marvin");
  });
});

Setup and Teardown

beforeEach

Runs before every test:

beforeEach(() => {
  // setup
});

afterEach

Runs after every test:

afterEach(() => {
  // cleanup
});

beforeAll

Runs once before all tests:

beforeAll(() => {
  // setup
});

afterAll

Runs once after all tests:

afterAll(() => {
  // cleanup
});

Example:

import { afterEach, beforeEach, describe, expect, it } from "vitest";

describe("users", () => {
  beforeEach(() => {
    // setup
  });

  afterEach(() => {
    // cleanup
  });

  it("creates a user", () => {
    // test
  });
});

Mock Functions

Use vi.fn():

import { expect, it, vi } from "vitest";

it("calls the function", () => {
  const fn = vi.fn();

  fn();

  expect(fn).toHaveBeenCalled();
});

Check arguments:

const fn = vi.fn();

fn("hello");

expect(fn).toHaveBeenCalledWith("hello");

Mock Return Values

const fn = vi.fn();

fn.mockReturnValue(42);

expect(fn()).toBe(42);

Async:

const fn = vi.fn();

fn.mockResolvedValue("hello");

expect(await fn()).toBe("hello");

Spying

Use vi.spyOn():

const user = {
  greet() {
    return "hello";
  },
};

const spy = vi.spyOn(user, "greet");

user.greet();

expect(spy).toHaveBeenCalled();

Restore the original function:

spy.mockRestore();

Mocking Modules

import { vi } from "vitest";

vi.mock("./database");

Example:

vi.mock("./database", () => ({
  getUser: vi.fn(() => ({
    id: 1,
    name: "Marvin",
  })),
}));

Skipping Tests

Skip a test:

it.skip("not implemented", () => {
  // ...
});

Skip a group:

describe.skip("unfinished tests", () => {
  // ...
});

Only Run Specific Tests

it.only("run this test", () => {
  // ...
});

Use .only temporarily while developing.

Test Files

Common naming conventions:

src/
├── math.ts
├── math.test.ts
├── user.ts
└── user.test.ts

Vitest also recognizes names such as:

*.test.ts
*.spec.ts
*.test.js
*.spec.js

Watch Mode

pnpm vitest

Vitest normally watches files during development.

Run once without watch mode:

pnpm vitest run

Test Coverage

Install the coverage package:

pnpm add -D @vitest/coverage-v8

Run:

pnpm vitest --coverage

Coverage shows how much of your code is exercised by tests.

Vitest Configuration

Create vitest.config.ts:

import { defineConfig } from "vitest/config";

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

With globals: true, you can use:

describe("math", () => {
  it("adds", () => {
    expect(1 + 1).toBe(2);
  });
});

without importing the test functions.

Without globals:

import { describe, expect, it } from "vitest";

Useful Commands

# Start watch mode
pnpm vitest

# Run once
pnpm vitest run

# Run a specific file
pnpm vitest math.test.ts

# Run tests matching a name
pnpm vitest -t "adds"

# Coverage
pnpm vitest --coverage

# UI
pnpm vitest --ui

Fundamental Vitest API

The most important things to learn first:

API Purpose
describe() Group tests
it() / test() Define a test
expect() Make assertions
beforeEach() Run setup before each test
afterEach() Run cleanup after each test
beforeAll() Run setup once
afterAll() Run cleanup once
vi.fn() Create mock functions
vi.spyOn() Spy on functions
vi.mock() Mock modules

Minimal Example

import { describe, expect, it } from "vitest";
import { add } from "./math";

describe("add", () => {
  it("adds two numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  it("handles negative numbers", () => {
    expect(add(-2, 3)).toBe(1);
  });
});

Run it:

pnpm vitest

The basic workflow is:

write code
   ↓
write test
   ↓
run Vitest
   ↓
assert expected behavior
   ↓
fix code
   ↓
repeat