Vitest: unit tests
describe/test/expect, the right matchers, mocks and fake timers.
Updated
What Vitest is
Vitest is a test runner: it finds the test files (*.test.ts), runs them and reports what passed and what didn't. It's fast, understands TypeScript and ESM without configuration and has a Jest-compatible API (the best-known runner).
npm i -D vitest
npx vitest # watch mode — reruns on every save
npx vitest run # a single run (CI)The structure
// src/shared/lib/price.test.ts
import { describe, expect, test } from 'vitest'
import { applyDiscount } from './price'
describe('applyDiscount', () => {
test('WELCOME10 takes off 10%', () => {
expect(applyDiscount(100, 'WELCOME10')).toBe(90)
})
test("an unknown code doesn't change the price", () => {
expect(applyDiscount(100, 'NOPE')).toBe(100)
})
})describegroups related tests.test(orit) — one case.expect(value).matcher(expected)— the check.
Put the test next to the file under test (colocation) — easy to find, it moves along with it.
Matchers — kinds and when
| Matcher | Compares | When |
|---|---|---|
toBe(x) |
identity (Object.is) |
primitives: numbers, strings, booleans |
toEqual(x) |
content, recursively | objects and arrays |
toStrictEqual(x) |
content + undefineds + class |
when exactness matters |
toBeTruthy() / toBeNull() / toBeUndefined() |
special values | — |
toContain(x) |
an element in an array / a substring | lists, texts |
toHaveLength(n) |
the length | lists |
toThrow(msg?) |
the function throws | validation |
resolves / rejects |
a Promise | async code |
expect({ a: 1 }).toBe({ a: 1 }) // ✗ different objects
expect({ a: 1 }).toEqual({ a: 1 }) // ✓ the same content
expect(() => parseAge(-1)).toThrow('Invalid age')
await expect(loadUser(1)).resolves.toEqual({ id: 1 })Mocks
A mock is a fake function that replaces a dependency (an API, time, randomness) so the test is fast and deterministic.
import { vi } from 'vitest'
const onSave = vi.fn() // a spy function
render(<Form onSave={onSave} />)
// ... the user submits the form
expect(onSave).toHaveBeenCalledWith({ email: 'a@x.com' })
vi.useFakeTimers() // you control time
vi.advanceTimersByTime(300) // e.g. for a debounce| Kind | What it does |
|---|---|
vi.fn() |
a function that records its calls |
vi.spyOn(obj, 'method') |
spies on an existing method |
vi.mock('./api') |
replaces a whole module |
vi.useFakeTimers() |
controlled time |
Don't overdo mocks: the more you mock, the less of reality you test. For HTTP requests, MSW intercepts at the network level — the code stays untouched.
Setting it up with Next.js
Next has an official guide for Vitest (with @vitejs/plugin-react and jsdom for components). Careful: async Server Components can't be unit-tested well yet — use E2E for them.
Summary
- Vitest runs
*.test.ts;describe/test/expect. toBefor primitives,toEqualfor objects.vi.fnand fake timers for dependencies; mock as little as possible.