Back to Blog
VitestTestingJavaScriptTypeScript

Testing with Vitest

Master Vitest for fast testing. From basic tests to mocking to component testing patterns.

B
Bootspring Team
Engineering
September 5, 2021
6 min read

Vitest provides fast, modern testing for JavaScript and TypeScript. Here's how to use it effectively.

Setup#

npm install -D vitest
1// 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.

Share this article

Help spread the word about Bootspring