Testing Fundamentals

Writing Your First Tests with Vitest

Get started with Vitest — the fast, modern test runner built for TypeScript and Vite-based projects.

Why Vitest

Vitest is the test runner of choice for modern TypeScript and Vite-based projects:

  • Fast — Vite-powered, runs tests in parallel, reruns only changed tests in watch mode
  • TypeScript-first — No configuration needed for TypeScript
  • Jest-compatible API — describe, it, expect work exactly like Jest
  • Native ESM support — No CommonJS transformation headaches

Installation and Setup

bash
npm install -D vitest

Configure in vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,          // Use describe/it without importing
    environment: 'node',    // or 'jsdom' for browser-like testing
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
    },
  },
});

Add scripts to package.json:

json
{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:coverage": "vitest --coverage"
  }
}

Test File Naming

Vitest discovers tests in files matching these patterns:

  • **/*.test.ts
  • **/*.spec.ts
  • Files inside **/__tests__/**

Keep test files next to the source files they test:

text
src/
  utils/
    formatPrice.ts
    formatPrice.test.ts   ← test file next to source
  components/
    Button.tsx
    Button.test.tsx

Basic Test Structure

typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { formatPrice, slugify, isValidEmail } from './utils';

// describe() groups related tests
describe('formatPrice', () => {

  // it() or test() defines an individual test
  it('formats cents as dollar amount', () => {
    // expect().toBe() is the most common assertion
    expect(formatPrice(1999)).toBe('$19.99');
  });

  it('formats zero correctly', () => {
    expect(formatPrice(0)).toBe('$0.00');
  });
});

describe('isValidEmail', () => {
  it('returns true for valid emails', () => {
    expect(isValidEmail('user@example.com')).toBe(true);
  });

  it('returns false for missing @ symbol', () => {
    expect(isValidEmail('notanemail')).toBe(false);
  });
});

Essential Assertions

typescript
// Equality
expect(2 + 2).toBe(4);                           // Exact equality (===)
expect({ a: 1 }).toEqual({ a: 1 });              // Deep equality
expect([1, 2, 3]).toContain(2);                  // Array contains value
expect('hello world').toMatch(/world/);           // String matches regex

// Truthiness
expect(true).toBeTruthy();
expect(null).toBeFalsy();
expect(undefined).toBeUndefined();
expect(null).toBeNull();

// Numbers
expect(10).toBeGreaterThan(5);
expect(3.14).toBeCloseTo(3.14159, 2);            // Float comparison

// Errors
expect(() => divide(1, 0)).toThrow('Cannot divide by zero');

// Async
await expect(fetchUser(123)).resolves.toEqual({ id: 123, name: 'Alice' });
await expect(fetchUser(-1)).rejects.toThrow('User not found');

Setup and Teardown

typescript
describe('UserService', () => {
  let db: TestDatabase;

  beforeAll(async () => {
    // Runs once before all tests in this describe block
    db = await createTestDatabase();
  });

  afterAll(async () => {
    // Runs once after all tests
    await db.destroy();
  });

  beforeEach(async () => {
    // Runs before each test — reset to clean state
    await db.seed({ users: [{ id: 1, email: 'alice@example.com' }] });
  });

  afterEach(async () => {
    // Runs after each test — clean up
    await db.truncate('users');
  });

  it('finds a user by email', async () => {
    const user = await UserService.findByEmail('alice@example.com');
    expect(user.id).toBe(1);
  });
});

Running Tests

bash
npx vitest              # Run all tests once
npx vitest --watch      # Watch mode — rerun on changes
npx vitest --coverage   # Run with coverage report
npx vitest utils        # Run tests in files matching "utils"

Key Takeaways

  • Vitest is the recommended test runner for TypeScript/Vite projects — fast, TypeScript-native, Jest-compatible API
  • Test files are co-located with source files using .test.ts or .spec.ts naming
  • describe() groups tests, it() defines individual tests, expect() makes assertions
  • beforeEach/afterEach ensure each test starts with clean, predictable state — never rely on test execution order
  • Watch mode (vitest --watch) provides instant feedback during development

Example

typescript
// src/utils/formatPrice.test.ts
import { describe, it, expect } from 'vitest';
import { formatPrice } from './formatPrice';

describe('formatPrice', () => {
  it('formats positive amounts correctly', () => {
    expect(formatPrice(1999)).toBe('$19.99');
    expect(formatPrice(100)).toBe('$1.00');
    expect(formatPrice(50)).toBe('$0.50');
  });

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('$0.00');
  });

  it('formats large amounts with commas', () => {
    expect(formatPrice(100000)).toBe('$1,000.00');
  });

  it('throws for negative amounts', () => {
    expect(() => formatPrice(-1)).toThrow('Amount cannot be negative');
  });
});
Try it yourself — TYPESCRIPT