Vitest provides fast, modern testing for JavaScript and TypeScript. Here's how to use it effectively.
Setup#
npm install -D vitest1// vite.config.ts
2import { defineConfig } from 'vite';
3
4export default defineConfig({
5 test: {
6 globals: true,
7 environment: 'node',
8 include: ['**/*.{test,spec}.{js,ts,tsx}'],
9 coverage: {
10 reporter: ['text', 'json', 'html'],
11 },
12 },
13});
14
15// package.json
16{
17 "scripts": {
18 "test": "vitest",
19 "test:run": "vitest run",
20 "test:coverage": "vitest run --coverage"
21 }
22}Basic Tests#
1import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
3// Simple assertion
4it('adds numbers correctly', () => {
5 expect(1 + 2).toBe(3);
6});
7
8// Grouped tests
9describe('Calculator', () => {
10 describe('add', () => {
11 it('adds positive numbers', () => {
12 expect(add(1, 2)).toBe(3);
13 });
14
15 it('adds negative numbers', () => {
16 expect(add(-1, -2)).toBe(-3);
17 });
18
19 it('handles zero', () => {
20 expect(add(5, 0)).toBe(5);
21 });
22 });
23});
24
25// Setup and teardown
26describe('Database', () => {
27 let db: Database;
28
29 beforeEach(async () => {
30 db = await createTestDatabase();
31 });
32
33 afterEach(async () => {
34 await db.close();
35 });
36
37 it('saves records', async () => {
38 await db.save({ id: 1, name: 'test' });
39 const record = await db.find(1);
40 expect(record.name).toBe('test');
41 });
42});Matchers#
1// Equality
2expect(value).toBe(3); // Strict equality
3expect(obj).toEqual({ a: 1 }); // Deep equality
4expect(obj).toStrictEqual({ a: 1 }); // Strict deep equality
5
6// Truthiness
7expect(value).toBeTruthy();
8expect(value).toBeFalsy();
9expect(value).toBeNull();
10expect(value).toBeUndefined();
11expect(value).toBeDefined();
12
13// Numbers
14expect(value).toBeGreaterThan(3);
15expect(value).toBeGreaterThanOrEqual(3);
16expect(value).toBeLessThan(5);
17expect(value).toBeCloseTo(0.3, 5); // Floating point
18
19// Strings
20expect(str).toMatch(/pattern/);
21expect(str).toContain('substring');
22expect(str).toHaveLength(10);
23
24// Arrays
25expect(array).toContain(item);
26expect(array).toContainEqual({ id: 1 });
27expect(array).toHaveLength(3);
28
29// Objects
30expect(obj).toHaveProperty('key');
31expect(obj).toHaveProperty('nested.key', 'value');
32expect(obj).toMatchObject({ partial: true });
33
34// Exceptions
35expect(() => badFunction()).toThrow();
36expect(() => badFunction()).toThrow('error message');
37expect(() => badFunction()).toThrow(CustomError);
38
39// Async
40await expect(asyncFn()).resolves.toBe('value');
41await expect(asyncFn()).rejects.toThrow('error');
42
43// Negation
44expect(value).not.toBe(3);
45expect(array).not.toContain(item);Mocking#
1import { vi, describe, it, expect, beforeEach } from 'vitest';
2
3// Mock functions
4const mockFn = vi.fn();
5mockFn('arg1', 'arg2');
6
7expect(mockFn).toHaveBeenCalled();
8expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
9expect(mockFn).toHaveBeenCalledTimes(1);
10
11// Mock return values
12const mock = vi.fn()
13 .mockReturnValue('default')
14 .mockReturnValueOnce('first')
15 .mockReturnValueOnce('second');
16
17console.log(mock()); // 'first'
18console.log(mock()); // 'second'
19console.log(mock()); // 'default'
20
21// Mock implementations
22const mockAsync = vi.fn().mockImplementation(async (id) => {
23 return { id, name: 'test' };
24});
25
26// Mock resolved/rejected values
27const mockPromise = vi.fn()
28 .mockResolvedValue({ data: 'success' })
29 .mockRejectedValueOnce(new Error('failed'));
30
31// Spy on object methods
32const obj = {
33 method: (x: number) => x * 2,
34};
35
36const spy = vi.spyOn(obj, 'method');
37obj.method(5);
38
39expect(spy).toHaveBeenCalledWith(5);
40spy.mockRestore();
41
42// Clear mocks
43beforeEach(() => {
44 vi.clearAllMocks(); // Clear call history
45 vi.resetAllMocks(); // Clear + reset implementations
46 vi.restoreAllMocks(); // Restore original implementations
47});Mocking Modules#
1import { vi, describe, it, expect } from 'vitest';
2import { fetchUser } from './api';
3import { sendEmail } from './email';
4
5// Mock entire module
6vi.mock('./api', () => ({
7 fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
8}));
9
10// Mock with factory
11vi.mock('./email', async (importOriginal) => {
12 const actual = await importOriginal<typeof import('./email')>();
13 return {
14 ...actual,
15 sendEmail: vi.fn().mockResolvedValue({ sent: true }),
16 };
17});
18
19describe('UserService', () => {
20 it('fetches user data', async () => {
21 const user = await fetchUser(1);
22 expect(user.name).toBe('Test');
23 });
24
25 it('sends welcome email', async () => {
26 await sendWelcomeEmail('test@example.com');
27 expect(sendEmail).toHaveBeenCalledWith(
28 'test@example.com',
29 expect.stringContaining('Welcome')
30 );
31 });
32});
33
34// Mock node modules
35vi.mock('axios', () => ({
36 default: {
37 get: vi.fn().mockResolvedValue({ data: { success: true } }),
38 post: vi.fn().mockResolvedValue({ data: { id: 1 } }),
39 },
40}));Testing Async Code#
1import { describe, it, expect, vi } from 'vitest';
2
3// Async/await
4it('fetches data', async () => {
5 const data = await fetchData();
6 expect(data).toEqual({ id: 1 });
7});
8
9// Promises
10it('resolves with data', () => {
11 return expect(fetchData()).resolves.toEqual({ id: 1 });
12});
13
14it('rejects with error', () => {
15 return expect(failingFetch()).rejects.toThrow('Network error');
16});
17
18// Timers
19describe('delayed functions', () => {
20 beforeEach(() => {
21 vi.useFakeTimers();
22 });
23
24 afterEach(() => {
25 vi.useRealTimers();
26 });
27
28 it('delays execution', () => {
29 const callback = vi.fn();
30 setTimeout(callback, 1000);
31
32 expect(callback).not.toHaveBeenCalled();
33
34 vi.advanceTimersByTime(1000);
35
36 expect(callback).toHaveBeenCalled();
37 });
38
39 it('runs all timers', () => {
40 const callback = vi.fn();
41 setTimeout(callback, 5000);
42 setInterval(callback, 1000);
43
44 vi.runAllTimers();
45
46 expect(callback).toHaveBeenCalled();
47 });
48});
49
50// Date mocking
51it('uses mocked date', () => {
52 vi.setSystemTime(new Date('2024-01-01'));
53
54 expect(new Date().getFullYear()).toBe(2024);
55
56 vi.useRealTimers();
57});Snapshot Testing#
1import { describe, it, expect } from 'vitest';
2
3it('matches snapshot', () => {
4 const result = generateReport();
5 expect(result).toMatchSnapshot();
6});
7
8it('matches inline snapshot', () => {
9 const obj = { name: 'test', value: 42 };
10 expect(obj).toMatchInlineSnapshot(`
11 {
12 "name": "test",
13 "value": 42,
14 }
15 `);
16});
17
18// Custom serializer
19expect.addSnapshotSerializer({
20 serialize(val, config, indentation, depth, refs, printer) {
21 return `User: ${val.name}`;
22 },
23 test(val) {
24 return val && typeof val === 'object' && 'name' in val;
25 },
26});Testing React Components#
1// Setup
2// vite.config.ts
3import { defineConfig } from 'vite';
4import react from '@vitejs/plugin-react';
5
6export default defineConfig({
7 plugins: [react()],
8 test: {
9 environment: 'jsdom',
10 setupFiles: ['./src/test/setup.ts'],
11 },
12});
13
14// setup.ts
15import '@testing-library/jest-dom/vitest';
16import { cleanup } from '@testing-library/react';
17import { afterEach } from 'vitest';
18
19afterEach(() => {
20 cleanup();
21});
22
23// Component test
24import { describe, it, expect, vi } from 'vitest';
25import { render, screen, fireEvent, waitFor } from '@testing-library/react';
26import userEvent from '@testing-library/user-event';
27import { Counter } from './Counter';
28
29describe('Counter', () => {
30 it('renders initial count', () => {
31 render(<Counter initialCount={5} />);
32 expect(screen.getByText('Count: 5')).toBeInTheDocument();
33 });
34
35 it('increments on click', async () => {
36 const user = userEvent.setup();
37 render(<Counter initialCount={0} />);
38
39 await user.click(screen.getByRole('button', { name: /increment/i }));
40
41 expect(screen.getByText('Count: 1')).toBeInTheDocument();
42 });
43
44 it('calls onChange callback', async () => {
45 const onChange = vi.fn();
46 const user = userEvent.setup();
47
48 render(<Counter initialCount={0} onChange={onChange} />);
49 await user.click(screen.getByRole('button', { name: /increment/i }));
50
51 expect(onChange).toHaveBeenCalledWith(1);
52 });
53});
54
55// Async component
56describe('UserProfile', () => {
57 it('loads user data', async () => {
58 render(<UserProfile userId="1" />);
59
60 expect(screen.getByText('Loading...')).toBeInTheDocument();
61
62 await waitFor(() => {
63 expect(screen.getByText('John Doe')).toBeInTheDocument();
64 });
65 });
66});Test Utilities#
1import { describe, it, expect, test } from 'vitest';
2
3// Parameterized tests
4describe('add', () => {
5 test.each([
6 [1, 2, 3],
7 [0, 0, 0],
8 [-1, 1, 0],
9 [100, 200, 300],
10 ])('add(%i, %i) = %i', (a, b, expected) => {
11 expect(add(a, b)).toBe(expected);
12 });
13
14 test.each`
15 a | b | expected
16 ${1} | ${2} | ${3}
17 ${0} | ${0} | ${0}
18 `('add($a, $b) = $expected', ({ a, b, expected }) => {
19 expect(add(a, b)).toBe(expected);
20 });
21});
22
23// Skipping and focusing
24describe.skip('skipped suite', () => {});
25it.skip('skipped test', () => {});
26
27describe.only('focused suite', () => {});
28it.only('focused test', () => {});
29
30// Todo tests
31it.todo('implement this later');
32
33// Concurrent tests
34describe.concurrent('parallel tests', () => {
35 it('test 1', async () => {});
36 it('test 2', async () => {});
37});
38
39// Retry flaky tests
40describe('flaky tests', () => {
41 it('might fail sometimes', { retry: 3 }, async () => {
42 const result = await unreliableOperation();
43 expect(result).toBe(true);
44 });
45});Best Practices#
Organization:
✓ Group related tests
✓ Use descriptive names
✓ Follow AAA pattern (Arrange, Act, Assert)
✓ Keep tests focused
Mocking:
✓ Mock external dependencies
✓ Reset mocks between tests
✓ Avoid mocking too much
✓ Use spies for verification
Performance:
✓ Use concurrent tests
✓ Minimize setup/teardown
✓ Run tests in watch mode
✓ Use coverage reports
Maintenance:
✓ Update snapshots intentionally
✓ Remove obsolete tests
✓ Refactor test code
✓ Document complex setups
Conclusion#
Vitest provides fast, modern testing with great TypeScript support and Jest compatibility. Use mocking strategically, leverage concurrent tests for speed, and keep tests focused and maintainable. The watch mode and coverage reports make development feedback loops quick and informative.