What is Vitest?
Vitest is a test runner powered by Vite. It reuses your existing Vite configuration — aliases, plugins, TypeScript and JSX transforms — so tests run through the same pipeline as your application. That shared foundation is why it starts fast and stays fast.
The API will feel familiar if you have used Jest. describe, it, expect and the mocking helpers all work the same way. What changes is the engine underneath: Vitest runs tests in parallel workers, watches files with Vite’s module graph and ships first-class TypeScript support without extra configuration.
Writing your first test
A test file imports from vitest and describes the behaviour it verifies.
// sum.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./sum";
describe("sum", () => {
it("adds two numbers", () => {
expect(sum(2, 3)).toBe(5);
});
it("handles negatives", () => {
expect(sum(-1, -1)).toBe(-2);
});
});
Name tests after the behaviour, not the function. A failing test that says “adds two numbers” tells you what broke; one that says “sum test 1” does not. Group related cases with describe and use beforeEach and afterEach for setup and cleanup.
Assertions
Vitest ships a rich matcher API, close to Jest’s.
// assertions.test.ts
expect(value).toBe(5); // strict equality
expect(value).toEqual({ a: 1 }); // deep equality
expect(value).toBeTruthy();
expect(list).toHaveLength(3);
expect(list).toContain("a");
expect(fn).toThrow("invalid");
expect(value).toBeGreaterThan(3);
Use toEqual for objects and arrays and toBe for primitives and reference checks. toMatchObject and toContainEqual are useful when you only care about part of a structure.
Mocks and spies
Isolation is central to unit testing. Vitest provides vi.fn for mocks, vi.spyOn for wrapping existing methods and vi.mock for replacing whole modules.
// api.test.ts
import { vi, describe, it, expect } from "vitest";
import { fetchUser } from "./api";
vi.mock("./http", () => ({
get: vi.fn().mockResolvedValue({ id: 1, name: "Ada" }),
}));
describe("fetchUser", () => {
it("returns the user", async () => {
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: "Ada" });
});
});
vi.mock is hoisted above imports, so the mock is registered before the module under test loads it. When the factory needs a variable defined in the test file, use vi.hoisted so the value exists before the mock runs.
Async testing
Await the operation and assert on the result. Vitest provides resolves and rejects helpers for promises.
// async.test.ts
it("rejects on failure", async () => {
await expect(loadUser(-1)).rejects.toThrow("Invalid id");
});
Prefer this style over the done callback. Forgetting done causes timeouts that are hard to diagnose, and unhandled rejections can leak between tests. For fake timers, vi.useFakeTimers() and vi.advanceTimersByTime() let you test debounces and intervals deterministically.
Snapshots
Snapshots capture a value and compare it on later runs. They are useful for large, stable output such as serialised data or rendered markup, but they are easy to abuse.
// snapshot.test.ts
it("matches the shape", () => {
expect(createConfig({ debug: true })).toMatchSnapshot();
});
A snapshot that is updated without reading it is worse than no test. Use them for data structures where a full comparison is impractical, and prefer explicit assertions when you know what matters. toMatchInlineSnapshot keeps the expected value in the test file, which makes reviews meaningful.
Configuration
Vitest reads a test block from your Vite config or a dedicated vitest.config.ts.
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
coverage: {
provider: "v8",
reporter: ["text", "html"],
thresholds: { lines: 80 },
},
},
});
The environment option selects node, jsdom, happy-dom or a browser. For component tests, jsdom or happy-dom is typical; for pure logic, keep the fast node environment. Coverage thresholds turn a coverage goal into an enforced rule.
Vitest and the testing pyramid
Vitest is the unit and integration layer. Pair it with a component-testing library such as Testing Library for UI behaviour, and with an end-to-end tool like Playwright for full user flows. Together they cover the pyramid: many fast unit tests, fewer integration tests and a handful of end-to-end tests.
Best practices
- Test behaviour, not implementation details.
- Keep each test focused on one behaviour.
- Use
beforeEachfor shared setup and clean up mocks withvi.clearAllMocks. - Prefer
resolvesandrejectsoverdonecallbacks. - Mock at the boundary (network, time, randomness), not every internal call.
- Use coverage thresholds to prevent silent regressions.
- Run in watch mode while developing and in CI on every push.
Common mistakes
- Asserting on internal state or private methods instead of observable behaviour.
- Updating snapshots without reviewing the diff.
- Forgetting to clear mocks between tests and leaking state.
- Using real timers and making tests slow and flaky.
- Over-mocking until the test only verifies the mocks.
- Testing the same behaviour at every level of the pyramid.
Where to go next
Vitest is the fastest way to start testing a Vite project. Compare it with the classic Jest, add Testing Library for components and Playwright for end-to-end flows, and lean on Vite to keep the configuration shared. Then write a few tests for code you already have and watch a refactor become safe.