48Vitest (modern unit testing)
What you will master here
- Why Vitest vs Jest
- Test structure: describe / it / expect
- Mocks, spies, fake timers
- Coverage with v8 / istanbul
- Snapshot testing
- React component testing with Vitest + Testing Library
- Watch mode + parallel + UI
48.1 Why Vitest
- ESM-native, super-fast (powered by Vite)
- Jest-compatible API — easy migration
- TypeScript first-class, no Babel config
- Built-in coverage, watch mode, UI, snapshot
- Workspaces for monorepos
npm i -D vitest @vitest/coverage-v8 @vitest/ui
48.2 First test
// src/sum.test.ts
import { describe, it, expect } from 'vitest';
import { sum } from './sum';
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('handles negatives', () => {
expect(sum(-1, -2)).toBe(-3);
});
});
npx vitest # watch mode by default npx vitest run # single run (CI) npx vitest --ui # interactive UI in browser npx vitest --coverage # with coverage
48.3 Expect API (Jest-compatible)
expect(value).toBe(x); // ===
expect(obj).toEqual({ a: 1 }); // deep equal
expect(obj).toMatchObject({ a: 1 }); // subset
expect(arr).toContain(x);
expect(arr).toHaveLength(3);
expect(fn).toThrow('boom');
expect(fn).toThrow(MyError);
expect(value).toBeDefined();
expect(value).toBeNull();
expect(value).toBeTruthy();
expect(value).toBeGreaterThan(5);
expect(value).toBeCloseTo(0.3, 5); // float
expect(str).toMatch(/regex/);
// Async
await expect(promise).resolves.toBe(42);
await expect(promise).rejects.toThrow();
48.4 Mocks & spies
import { vi, expect, it } from 'vitest';
// Spy on an existing object
const logger = { info: (m: string) => m };
const spy = vi.spyOn(logger, 'info');
logger.info('hello');
expect(spy).toHaveBeenCalledWith('hello');
// Standalone mock function
const fn = vi.fn().mockReturnValue(42);
expect(fn()).toBe(42);
expect(fn).toHaveBeenCalledOnce();
// Mock a module
vi.mock('node:fs', () => ({
readFileSync: vi.fn(() => 'mocked contents'),
}));
// Reset between tests
beforeEach(() => vi.clearAllMocks());
48.5 Fake timers
it('debounce fires once', () => {
vi.useFakeTimers();
const fn = vi.fn();
const d = debounce(fn, 300);
d(); d(); d();
vi.advanceTimersByTime(299);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledOnce();
vi.useRealTimers();
});
48.6 Snapshots
it('renders user card', () => {
const html = render(<UserCard name="Alice" />).container.innerHTML;
expect(html).toMatchSnapshot();
});
// Inline
expect(value).toMatchInlineSnapshot(`
{ "a": 1, "b": 2 }
`);
Run npx vitest -u to update snapshots when output changes intentionally.
48.7 React component testing
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: { environment: 'jsdom', setupFiles: ['./test-setup.ts'] },
});
// Button.test.tsx
import { render, screen, userEvent } from '@testing-library/react';
import { Button } from './Button';
it('fires onClick', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Buy</Button>);
await userEvent.click(screen.getByRole('button', { name: 'Buy' }));
expect(onClick).toHaveBeenCalledOnce();
});
48.8 Vitest vs Jest
| Aspect | Vitest | Jest |
|---|---|---|
| ESM | Native | Workarounds needed |
| Speed | Faster (Vite) | Slower |
| API | Jest-compatible | Original |
| Config | Same as Vite | jest.config.js |
| Ecosystem | Younger but growing | Huge |
Module 57 — Vitest Q&A
Why pick Vitest over Jest?
ESM-native, integrated with Vite's transform pipeline (faster, no Babel), Jest-compatible API for easy migration. For Vite-based projects (React/Vue/Svelte modern setups), Vitest is the default choice now.
What's the difference between toBe and toEqual?
toBe: strict equality (===). toEqual: deep value equality on objects/arrays.
toBe({a:1}) fails because different references; toEqual({a:1}) passes.How do you test an async function?
Two ways:
await expect(promise).resolves.toBe(x), or await the value first then expect normally. For rejection: .rejects.toThrow().What is a snapshot test?
A test that compares the current output to a saved snapshot file. Useful for HTML/JSON output that's hard to assert field-by-field. Update with
vitest -u when output changes intentionally.How do you fake timers?
vi.useFakeTimers(). Then vi.advanceTimersByTime(N) moves time forward; setTimeout/setInterval fire at synthetic times. Restore real timers in afterEach.How do you mock a module?
vi.mock('module', () => ({ exportName: vi.fn() })) at the top of the test file. Vitest hoists it before imports. Use vi.unmock or vi.doMock for finer control.When to write a unit test vs E2E in Playwright?
Unit (Vitest): pure logic, transformations, utility functions — fast, isolated. E2E (Playwright): user journeys, integration, browser-specific behaviour. Most logic should be unit-tested; reserve E2E for what actually requires a browser.