DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Vitest: Faster JavaScript Testing with ESM Support, React Components, and Coverage

Vitest has replaced Jest as the go-to testing framework for modern JavaScript projects. Faster, ESM-native, and with a nearly identical API — the migration is low-friction and the performance gains are real.

Why Vitest Over Jest

  • 10-20x faster: Runs tests in parallel using Vite's transform pipeline
  • ESM-native: No transform config needed for modern JS
  • In-source testing: Write tests in the same file as the code
  • Compatible API: describe, it, expect work the same as Jest
  • Built-in coverage: No separate @jest/coverage package

Setup

npm install -D vitest @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom', // for React component tests
    globals: true, // no need to import describe/it/expect
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['node_modules/', 'tests/', '*.config.*'],
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Testing Pure Functions

// lib/pricing.test.ts
import { describe, it, expect } from 'vitest'
import { calculateDiscount, formatPrice } from './pricing'

describe('calculateDiscount', () => {
  it('applies percentage discount', () => {
    expect(calculateDiscount(100, { type: 'percent', value: 20 })).toBe(80)
  })

  it('applies fixed discount', () => {
    expect(calculateDiscount(100, { type: 'fixed', value: 15 })).toBe(85)
  })

  it('does not go below zero', () => {
    expect(calculateDiscount(10, { type: 'fixed', value: 50 })).toBe(0)
  })

  it('rejects negative discount values', () => {
    expect(() => calculateDiscount(100, { type: 'percent', value: -5 })).toThrow()
  })
})
Enter fullscreen mode Exit fullscreen mode

Testing API Routes

// app/api/users/route.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { GET } from './route'
import { db } from '@/lib/db'

vi.mock('@/lib/db', () => ({
  db: {
    user: {
      findMany: vi.fn(),
    },
  },
})) 

vi.mock('next-auth', () => ({
  getServerSession: vi.fn().mockResolvedValue({ user: { id: 'user-1' } }),
}))

describe('GET /api/users', () => {
  beforeEach(() => vi.clearAllMocks())

  it('returns users for authenticated request', async () => {
    vi.mocked(db.user.findMany).mockResolvedValue([{ id: '1', name: 'Alice' }])

    const req = new Request('http://localhost/api/users')
    const res = await GET(req)
    const body = await res.json()

    expect(res.status).toBe(200)
    expect(body).toHaveLength(1)
    expect(body[0].name).toBe('Alice')
  })
})
Enter fullscreen mode Exit fullscreen mode

Testing React Components

// components/PricingCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { PricingCard } from './PricingCard'

describe('PricingCard', () => {
  const defaultProps = {
    name: 'Pro',
    price: 99,
    features: ['Feature 1', 'Feature 2'],
    onSelect: vi.fn(),
  }

  it('renders plan name and price', () => {
    render(<PricingCard {...defaultProps} />)
    expect(screen.getByText('Pro')).toBeInTheDocument()
    expect(screen.getByText('$99')).toBeInTheDocument()
  })

  it('calls onSelect when CTA is clicked', () => {
    render(<PricingCard {...defaultProps} />)
    fireEvent.click(screen.getByRole('button', { name: /get started/i }))
    expect(defaultProps.onSelect).toHaveBeenCalledWith('Pro')
  })
})
Enter fullscreen mode Exit fullscreen mode

Snapshot Testing

it('matches snapshot', () => {
  const result = formatUserData({ name: 'Alice', role: 'admin' })
  expect(result).toMatchInlineSnapshot(`
    {
      "displayName": "Alice",
      "permissions": ["read", "write", "delete"],
    }
  `)
})
Enter fullscreen mode Exit fullscreen mode

Inline snapshots are stored in the test file itself — no separate __snapshots__ directory.

Coverage Enforcement

// package.json
{
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "test:ci": "vitest --coverage --reporter=junit"
  }
}
Enter fullscreen mode Exit fullscreen mode
// vitest.config.ts -- fail CI if coverage drops
coverage: {
  thresholds: {
    lines: 80,
    functions: 80,
    branches: 70,
  },
}
Enter fullscreen mode Exit fullscreen mode

The Ship Fast Skill Pack at whoffagents.com includes a /test skill that generates Vitest test suites for any function, React component, or API route. $49 one-time.

Top comments (0)