Skip to main content

Vitest Complete Guide: Lightning-Fast Test Runner

Created: February 22, 2026 Larry Qu 4 min read

Introduction

Vitest is a blazing-fast test runner built on Vite. It provides a Jest-compatible API with native ESM support and incredible speed. This guide covers everything you need to know.

Why Vitest?

Feature Vitest Jest
Speed ⚡⚡⚡ Very fast ⚡ Fast
ESM Native Requires config
HMR Native Limited
Vite Built-in Plugin needed
TypeScript Native Requires setup

Getting Started

Installation

# Install Vitest
npm install -D vitest

# With Vite
npm create vite@latest my-app -- --template vue-ts
npm install -D vitest @vitejs/plugin-vue

Configuration

// vite.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    include: ['tests/**/*.test.ts'],
  },
})

Package.json Scripts

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

Basic Tests

Test Structure

import { describe, it, expect } from 'vitest'

describe('Math', () => {
  it('should add numbers', () => {
    expect(1 + 1).toBe(2)
  })

  it('should multiply', () => {
    expect(3 * 4).toBe(12)
  })
})

With globals (no imports)

// With globals: true in config

describe('Math', () => {
  it('adds correctly', () => {
    expect(1 + 1).toBe(2)
  })
})

Assertions

Common Matchers

// Equality
expect(value).toBe(2)
expect(value).toEqual({ a: 1 })

// Truthiness
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeUndefined()

// Numbers
expect(value).toBeGreaterThan(10)
expect(value).toBeLessThan(10)
expect(value).toBeCloseTo(3.14, 2)

// Strings
expect('hello').toContain('lo')
expect('hello').toMatch(/ell/)
expect('hello').toHaveLength(5)

// Arrays
expect([1, 2, 3]).toContain(2)
expect([1, 2, 3]).toHaveLength(3)

// Objects
expect({ a: 1 }).toHaveProperty('a')
expect({ a: 1 }).toMatchObject({ a: 1 })

Async Tests

it('async function', async () => {
  const result = await fetchUser(1)
  expect(result.name).toBe('John')
})

it('promise resolves', async () => {
  await expect(Promise.resolve('hello')).resolves.toBe('hello')
})

it('promise rejects', async () => {
  await expect(Promise.reject('error')).rejects.toBe('error')
})

Setup & Teardown

beforeEach(() => {
  // Runs before each test
  console.log('Setup')
})

afterEach(() => {
  // Runs after each test
  console.log('Teardown')
})

beforeAll(() => {
  // Runs once before all tests
  setupDatabase()
})

afterAll(() => {
  // Runs once after all tests
  closeDatabase()
})

Mocking

Functions

import { vi, describe, it, expect } from 'vitest'

// Mock function
const fn = vi.fn()

fn('hello')
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledWith('hello')

// Return value
fn.mockReturnValue(42)
expect(fn()).toBe(42)

// Implementation
fn.mockImplementation((x: number) => x * 2)
expect(fn(5)).toBe(10)

Modules

import { vi } from 'vitest'

// Mock module
vi.mock('./utils', async () => {
  const actual = await vi.importActual('./utils')
  return {
    ...actual,
    fetchUser: vi.fn().mockResolvedValue({ name: 'Mocked' }),
  }
})

// Partial mock
vi.mock('./utils', () => ({
  fetchUser: vi.fn().mockResolvedValue({ name: 'Mocked' }),
}))

Timers

import { vi, it, expect } from 'vitest'

// Fake timers
it('debounce', async () => {
  vi.useFakeTimers()
  
  const fn = vi.fn()
  const debounced = debounce(fn, 1000)
  
  debounced()
  debounced()
  debounced()
  
  expect(fn).not.toHaveBeenCalled()
  
  vi.runAllTimers()
  
  expect(fn).toHaveBeenCalledTimes(1)
  
  vi.useRealTimers()
})

Components

Vue Testing

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Button from './Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: {
        label: 'Click me',
      },
    })
    
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' },
    })
    
    await wrapper.trigger('click')
    
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

React Testing

import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'

describe('Counter', () => {
  it('increments count', () => {
    render(<Counter />)
    
    const button = screen.getByText('Count: 0')
    fireEvent.click(button)
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })
})

Coverage

Configuration

// vite.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8', // or 'istanbul'
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.d.ts'],
    },
  },
})
# Run coverage
npm run test:coverage

Vitest vs Jest

Feature Vitest Jest
Speed Faster (Vite) Fast
HMR Instant Limited
ESM Native Complex
Workers Native Optional
Setup Easy Easy

Best Practices

1. Name Tests Clearly

// ✅ Good - descriptive names
describe('UserService.create', () => {
  it('creates a new user with valid data', async () => {
    const user = await createUser({ name: 'John', email: '[email protected]' })
    expect(user.id).toBeDefined()
  })
})

// ❌ Bad - vague names
describe('UserService', () => {
  it('create', async () => {
    expect(createUser({})).toBeTruthy()
  })
})

2. One Expectation Per Test

// ✅ Good - focused tests
it('validates email format', () => {
  expect(validateEmail('invalid')).toBe(false)
})

it('accepts valid email', () => {
  expect(validateEmail('[email protected]')).toBe(true)
})

// ❌ Bad - multiple concerns
it('user operations', () => {
  expect(validateEmail('invalid')).toBe(false)
  expect(createUser({})).toBeTruthy()
  expect(deleteUser(1)).toBeTruthy()
})

3. Use Test.each for Similar Tests

test.each([
  [1, 1, 2],
  [2, 3, 5],
  [10, 10, 20],
])('adds %i + %i to equal %i', (a, b, expected) => {
  expect(a + b).toBe(expected)
})

Conclusion

Vitest is excellent when you:

  • Already use Vite
  • Want blazing-fast tests
  • Need native ESM support
  • Prefer simple setup
  • Want Jest-compatible API

Perfect for: Vite projects, Vue/React apps, modern TypeScript projects.


External Resources

Resources

Comments

Share this article

Scan to read on mobile