DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Vitest Complete Guide: Fast Unit Testing for JavaScript (2026)

Vitest is a unit test framework built by the Vite team. It reuses your existing Vite config — TypeScript, JSX, path aliases, and environment variables all work in tests without extra setup. It implements nearly the entire Jest API, so migrating existing test suites is usually a matter of swapping the import.

Why Vitest Instead of Jest

A typical Jest setup for TypeScript + Vite needs:

  • babel-jest or ts-jest to handle TypeScript
  • A Babel config or jest.config.ts with transform
  • Separate handling for ESM packages
  • Path alias replication (your vite.config.ts aliases don't work in Jest)

With Vitest: none of that. Your existing vite.config.ts is already the Vitest config.

Installation

npm install -D vitest
Enter fullscreen mode Exit fullscreen mode

Add to package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "coverage": "vitest run --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configuration

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      thresholds: { lines: 80, functions: 80 }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Basic Test Structure

import { describe, it, expect } from 'vitest'
import { slugify, truncate } from './string-utils'

describe('slugify', () => {
  it('converts spaces to hyphens', () => {
    expect(slugify('Hello World')).toBe('hello-world')
  })

  it('removes special characters', () => {
    expect(slugify('TypeScript: The Good Parts!')).toBe('typescript-the-good-parts')
  })
})

describe('truncate', () => {
  it('truncates and appends ellipsis', () => {
    expect(truncate('hello world', 8)).toBe('hello...')
  })
})
Enter fullscreen mode Exit fullscreen mode

Mocking with vi

The vi object replaces jest:

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

// Mock a function
const mockFn = vi.fn().mockReturnValue('result')
expect(mockFn).toHaveBeenCalledOnce()

// Mock entire module
vi.mock('./database', () => ({
  db: {
    user: { findUnique: vi.fn(), create: vi.fn() }
  }
}))

// Partial mock — keep real implementation, mock one export
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./utils')>()
  return { ...actual, sendEmail: vi.fn() }
})

// Fake timers
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-15'))
vi.advanceTimersByTime(24 * 60 * 60 * 1000)
vi.useRealTimers()
Enter fullscreen mode Exit fullscreen mode

React Component Testing

npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode
// src/test/setup.ts
import '@testing-library/jest-dom'
Enter fullscreen mode Exit fullscreen mode
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import SearchBar from './SearchBar'

describe('SearchBar', () => {
  it('calls onSearch with input value on submit', async () => {
    const user = userEvent.setup()
    const onSearch = vi.fn()
    render(<SearchBar onSearch={onSearch} />)

    await user.type(screen.getByRole('textbox'), 'vitest guide')
    await user.click(screen.getByRole('button', { name: /search/i }))

    expect(onSearch).toHaveBeenCalledWith('vitest guide')
  })

  it('shows no results message when empty', async () => {
    const user = userEvent.setup()
    render(<SearchBar onSearch={vi.fn().mockResolvedValue([])} />)

    await user.type(screen.getByRole('textbox'), 'xyzabc')
    await user.click(screen.getByRole('button', { name: /search/i }))

    await waitFor(() => {
      expect(screen.getByText(/no results/i)).toBeInTheDocument()
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Vitest UI

npm install -D @vitest/ui
npx vitest --ui
Enter fullscreen mode Exit fullscreen mode

Opens a browser-based dashboard at http://localhost:51204 with live test results, filterable test tree, and console output per test.

Vitest vs Jest

Vitest Jest
Speed Fast (esbuild) Slower (Babel)
TypeScript Native with Vite Needs config
ESM Native Needs workarounds
Config Extends vite.config.ts Separate config
API Jest-compatible Original
Mocking vi.* jest.*
Browser testing @vitest/browser jsdom only

Migration from Jest is mostly mechanical:

  • Replace jest.* with vi.*
  • Remove Jest config and Babel transform setup
  • Add globals: true in Vitest config
  • Most test code runs without changes

Full article at stacknotice.com/blog/vitest-complete-guide-2026

Top comments (0)