Vitest Cheatsheet

Unit vs Integration Tests

Use this Vitest reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Definitions

Unit TestIntegration Test
ScopeSingle function, class, or moduleMultiple modules or layers working together
DependenciesAll external deps mockedReal or real-ish deps (DB, HTTP, FS)
SpeedVery fast (ms)Slower (hundreds of ms to seconds)
IsolationCompletePartial
Failure signalPinpoints exact functionReveals wiring/contract bugs
Typical countManyFewer

Unit Test Example

// math.ts
export function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max)
}
// math.test.ts
import { describe, it, expect } from 'vitest'
import { clamp } from './math'

describe('clamp', () => {
  it('returns value when in range', () => expect(clamp(5, 1, 10)).toBe(5))
  it('clamps to min', () => expect(clamp(-5, 1, 10)).toBe(1))
  it('clamps to max', () => expect(clamp(15, 1, 10)).toBe(10))
})

No mocks needed — pure function, no side effects.

Integration Test Example

// userService.ts — depends on DB and email service
export async function registerUser(email: string, password: string) {
  const user = await db.users.create({ email, password: hash(password) })
  await emailService.sendWelcome(user.email)
  return user
}
// userService.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { db } from './db'
import { registerUser } from './userService'

// Uses a real test DB, mocks only the email service
vi.mock('./emailService', () => ({ sendWelcome: vi.fn() }))

beforeAll(async () => { await db.migrate() })
afterAll(async () => { await db.destroy() })

describe('registerUser integration', () => {
  it('persists user to DB', async () => {
    const user = await registerUser('alice@test.com', 'pass')
    const found = await db.users.findByEmail('alice@test.com')
    expect(found).toMatchObject({ email: 'alice@test.com' })
  })
})

Structuring Test Files

src/
  services/
    userService.ts
    userService.unit.test.ts        # mocks DB, tests logic
    userService.integration.test.ts # real DB, tests wiring
  utils/
    format.ts
    format.test.ts                  # pure — always unit

Or use separate directories:

src/
tests/
  unit/
    userService.test.ts
  integration/
    userService.test.ts

Separating Test Runs in Config

// vitest.config.ts
export default defineConfig({
  test: {
    include: ['src/**/*.unit.test.ts'],
  },
})

// vitest.integration.config.ts
export default defineConfig({
  test: {
    include: ['src/**/*.integration.test.ts'],
    testTimeout: 30_000,
    globalSetup: './tests/globalSetup.ts',
  },
})
// package.json
{
  "scripts": {
    "test:unit":        "vitest run",
    "test:integration": "vitest run --config vitest.integration.config.ts"
  }
}

Using Projects to Run Both

test.projects (the replacement for the removed vitest.workspace.ts):

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['**/*.unit.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'integration',
          include: ['**/*.integration.test.ts'],
          testTimeout: 30_000,
          globalSetup: './tests/db-setup.ts',
        },
      },
    ],
  },
})

What to Mock in Each Test Type

Unit tests — mock everything external:

vi.mock('../db')
vi.mock('../emailService')
vi.mock('axios')

it('calls db.create with hashed password', async () => {
  vi.mocked(db.users.create).mockResolvedValue({ id: '1', email: 'a@a.com' })
  const user = await registerUser('a@a.com', 'plain')
  expect(db.users.create).toHaveBeenCalledWith(
    expect.objectContaining({ email: 'a@a.com' })
  )
})

Integration tests — mock only infrastructure boundaries:

// Don't mock the DB — that's what we're testing
// DO mock: external APIs, email, payment processors, file system
vi.mock('../emailService')
vi.mock('../stripe')

Test Pyramid Strategy

        /\
       /E2E\       — few, slow, expensive (Playwright/Cypress)
      /------\
     /Integr. \    — moderate, test wiring
    /----------\
   /   Unit     \  — many, fast, test logic
  /--------------\

Aim for ~70% unit / ~20% integration / ~10% E2E as a starting point.

Environment Config per Test Type

// vitest.config.ts
export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit-dom',
          environment: 'jsdom',
          include: ['**/*.unit.test.tsx'],
        },
      },
      {
        test: {
          name: 'integration-node',
          environment: 'node',
          include: ['**/*.integration.test.ts'],
        },
      },
    ],
  },
})

Integration Test Gotchas

  • Database state leakage — always truncate or roll back between tests.
  • Port conflicts — use dynamic ports or pool: 'forks' to isolate.
  • Slow CI — run integration tests in a separate job, cache the DB image.
  • Non-deterministic order — integration tests must be independent; never rely on insertion order.