Skip to main content
โšก Calmops

Vitest Complete Guide: Lightning-Fast Test Runner

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

Comments