Unit Testing with Vitest
Vitest is a lightning-fast test runner for JavaScript and TypeScript, designed to work seamlessly with Vite. If you have used Jest, you will feel right at home — but with better performance and modern defaults.
In this tutorial, you will learn how to set up Vitest, write your first tests, and explore features like snapshot testing, mocking, and test coverage.
Why Vitest?
Vitest offers several advantages over traditional test runners:
- Near-instant hot module replacement (HMR) — tests run in the same process as your dev server
- Native ES modules — no need for Babel or complex transformations
- Jest-compatible API — most Jest tests work with zero changes
- TypeScript support out of the box — no additional configuration needed
Setting Up Vitest
First, install Vitest in your project:
npm install -D vitest
Add a test script to your package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
Create a vitest.config.js in your project root:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['**/*.test.js', '**/*.spec.js'],
},
});
Now create your first test file:
// sum.test.js
import { describe, it, expect } from 'vitest';
function sum(a, b) {
return a + b;
}
describe('sum()', () => {
it('adds two numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});
it('handles negative numbers', () => {
expect(sum(-1, -1)).toBe(-2);
});
});
Run your tests:
npm test
Understanding Vitest Globals
Vitest provides global APIs by default — no imports needed:
// These are available globally
describe('Math operations', () => {
it('multiplies numbers', () => {
expect(3 * 4).toBe(12);
});
it('checks equality', () => {
expect({ a: 1 }).toEqual({ a: 1 });
});
it('checks truthiness', () => {
expect('hello').toBeTruthy();
expect(null).toBeFalsy();
});
});
If you prefer explicit imports, disable globals in your config:
export default defineConfig({
test: {
globals: false,
},
});
Test Matchers
Vitest includes all Jest-compatible matchers plus some extras:
import { it, expect } from 'vitest';
it('demonstrates common matchers', () => {
// Equality
expect(10).toBe(10);
expect({ a: 1 }).toEqual({ a: 1 });
// Truthiness
expect('test').toBeTruthy();
expect('').toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Numbers
expect(10).toBeGreaterThan(5);
expect(10).toBeLessThan(20);
expect(10).toBeGreaterThanOrEqual(10);
// Strings
expect('hello world').toContain('hello');
expect('hello').toMatch(/^hel/);
// Arrays
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
});
Async Testing
Testing async code is straightforward with Vitest:
import { it, expect } from 'vitest';
function fetchUser(id) {
return Promise.resolve({ id, name: 'Alice' });
}
it('handles async functions', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
it('handles promises with resolves/rejects', () => {
expect(Promise.resolve('success')).resolves.toBe('success');
expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
});
Mocking
Vitest includes a powerful mocking API:
import { vi, it, expect } from 'vitest';
// Mock a function
const fetchData = vi.fn(() => Promise.resolve({ data: 'mocked' }));
it('mocks a function', async () => {
const result = await fetchData();
expect(fetchData).toHaveBeenCalled();
expect(result.data).toBe('mocked');
});
// Mock modules
vi.mock('./api', () => ({
getUser: () => Promise.resolve({ name: 'Mocked User' }),
}));
// Mock timers
it('tests setTimeout', async () => {
vi.useFakeTimers();
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
});
Snapshot Testing
Snapshots capture rendered output and compare against saved versions:
import { it, expect } from 'vitest';
function formatUser(user) {
return 'Name: ' + user.name + ', Age: ' + user.age;
}
it('matches snapshot', () => {
const output = formatUser({ name: 'Alice', age: 30 });
expect(output).toMatchSnapshot();
});
// Update snapshots with: vitest update
Running Tests
Vitest provides several ways to run tests:
# Watch mode (default in dev)
npm test
# Run once
npm run test:run
# Run specific file
npx vitest run src/utils.test.js
# Run with coverage
npx vitest run --coverage
# Run tests matching a pattern
npx vitest run -t "adds two numbers"
Configuration Options
Here is a comprehensive vitest.config.js example:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// Test environment
environment: 'node', // or 'jsdom', 'happy-dom'
// Glob patterns for test files
include: ['**/*.test.js', '**/*.spec.js'],
// Exclude patterns
exclude: ['**/node_modules/**', '**/dist/**'],
// Global setup
setupFiles: ['./vitest.setup.js'],
// Coverage configuration
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
// Test timeout (ms)
testTimeout: 5000,
// Hook timeout
hookTimeout: 10000,
},
});
Summary
You have learned how to:
- Set up Vitest in your project
- Write tests using the Jest-compatible API
- Use matchers for assertions
- Test async code with promises
- Mock functions and modules
- Create snapshot tests
- Configure Vitest for different scenarios
Vitest speed and developer experience make it an excellent choice for modern JavaScript projects. The zero-config setup and Vite integration mean you can start testing in seconds.
In the next tutorial, we will explore testing asynchronous code in more depth.