Testing

Comprehensive testing guide for ShipKit using Vitest, React Testing Library, and best practices

Testing

ShipKit uses Vitest as its testing framework, along with React Testing Library for component testing. This guide covers setup, implementation, and best practices.

Setup

Vitest Configuration

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

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./src/test/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      exclude: [
        "node_modules/**",
        "src/test/**",
        "**/*.d.ts",
        "**/*.config.ts",
        "**/types/**",
      ],
    },
  },
})

Browser Testing Configuration

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

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: "jsdom",
    globals: true,
  },
})

Component Testing

Basic Component Test

// vitest-example/HelloWorld.test.tsx
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import HelloWorld from './HelloWorld'

test('renders name', async () => {
  const { getByText } = render(<HelloWorld name="Vitest" />)
  await expect.element(getByText('Hello Vitest!')).toBeInTheDocument()
})

Component Implementation

// vitest-example/HelloWorld.tsx
interface Props {
  name: string
}

export default function HelloWorld({ name }: Props) {
  return <div>Hello {name}!</div>
}

Testing Patterns

Unit Testing

// src/tests/utils/string.test.ts
import { describe, it, expect } from 'vitest'
import { formatString, validateString } from '@/utils/string'

describe('String Utils', () => {
  describe('formatString', () => {
    it('should capitalize first letter', () => {
      expect(formatString('hello')).toBe('Hello')
    })

    it('should handle empty string', () => {
      expect(formatString('')).toBe('')
    })
  })

  describe('validateString', () => {
    it('should validate string length', () => {
      expect(validateString('test', { minLength: 3 })).toBe(true)
      expect(validateString('a', { minLength: 3 })).toBe(false)
    })
  })
})

Integration Testing

// src/tests/features/auth.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { AuthProvider } from '@/providers/auth'
import { LoginForm } from '@/components/auth/login-form'

describe('Authentication', () => {
  beforeEach(() => {
    render(
      <AuthProvider>
        <LoginForm />
      </AuthProvider>
    )
  })

  it('should handle login submission', async () => {
    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: '[email protected]' },
    })
    fireEvent.change(screen.getByLabelText('Password'), {
      target: { value: 'password123' },
    })
    fireEvent.click(screen.getByText('Sign In'))

    await expect(screen.findByText('Welcome back!')).resolves.toBeInTheDocument()
  })
})

API Testing

// src/tests/api/users.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { createMocks } from 'node-mocks-http'
import { GET, POST } from '@/app/api/users/route'

describe('Users API', () => {
  describe('GET /api/users', () => {
    it('should return users list', async () => {
      const { req, res } = createMocks({
        method: 'GET',
      })

      await GET(req)

      expect(res._getStatusCode()).toBe(200)
      const data = JSON.parse(res._getData())
      expect(Array.isArray(data.users)).toBe(true)
    })
  })

  describe('POST /api/users', () => {
    it('should create new user', async () => {
      const { req, res } = createMocks({
        method: 'POST',
        body: {
          email: '[email protected]',
          name: 'Test User',
        },
      })

      await POST(req)

      expect(res._getStatusCode()).toBe(201)
      const data = JSON.parse(res._getData())
      expect(data.user.email).toBe('[email protected]')
    })
  })
})

Test Environment

Environment Variables

# .env.test
DATABASE_URL="postgresql://test:test@localhost:5432/test"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="test-secret"
OPENAI_API_KEY="test-key"
RESEND_API_KEY="test-key"
STRIPE_SECRET_KEY="test-key"
STRIPE_WEBHOOK_SECRET="test-secret"

Test Setup

// src/test/setup.ts
import '@testing-library/jest-dom'
import { vi } from 'vitest'
import { mockDB } from './mocks/db'
import { mockAuth } from './mocks/auth'

// Mock environment variables
vi.mock('@/env.mjs', () => ({
  env: {
    DATABASE_URL: 'test-url',
    NEXTAUTH_URL: 'http://localhost:3000',
    NEXTAUTH_SECRET: 'test-secret',
  },
}))

// Mock database
vi.mock('@/server/db', () => mockDB)

// Mock authentication
vi.mock('next-auth', () => mockAuth)

// Clean up after each test
afterEach(() => {
  vi.clearAllMocks()
})

Best Practices

  1. Test Organization

    • Group related tests using describe
    • Use clear test descriptions
    • Follow AAA pattern (Arrange, Act, Assert)
    • Keep tests focused and isolated
  2. Component Testing

    • Test user interactions
    • Verify rendered content
    • Check component states
    • Test error scenarios
  3. API Testing

    • Test request validation
    • Check response formats
    • Handle error cases
    • Mock external services
  4. Database Testing

    • Use test database
    • Clean up after tests
    • Mock database calls
    • Test transactions

Test Scripts

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage",
    "test:browser": "vitest --config vitest.config.browser.ts",
    "test:e2e": "playwright test"
  }
}

Error Handling

// src/tests/utils/error-handling.test.ts
import { describe, it, expect } from 'vitest'
import { handleError } from '@/utils/error-handling'

describe('Error Handling', () => {
  it('should handle known errors', () => {
    const error = new Error('Test error')
    error.code = 'KNOWN_ERROR'

    const result = handleError(error)
    expect(result.message).toBe('Test error')
    expect(result.code).toBe('KNOWN_ERROR')
  })

  it('should handle unknown errors', () => {
    const error = new Error('Unknown error')

    const result = handleError(error)
    expect(result.message).toBe('An unexpected error occurred')
    expect(result.code).toBe('UNKNOWN_ERROR')
  })
})

Mocking

// src/tests/mocks/auth.ts
export const mockAuth = {
  getSession: vi.fn(() => ({
    user: {
      id: '1',
      email: '[email protected]',
      name: 'Test User',
    },
  })),
  signIn: vi.fn(),
  signOut: vi.fn(),
}

// src/tests/mocks/db.ts
export const mockDB = {
  user: {
    findUnique: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  },
  post: {
    findMany: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  },
}

Coverage Reports

# Run coverage report
pnpm test:coverage

# Coverage output
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files          |   85.71 |    76.92 |   83.33 |   85.71 |
 src/utils/string  |  100.00 |   100.00 |  100.00 |  100.00 |
 src/utils/number  |   75.00 |    66.67 |   66.67 |   75.00 | 15,27-28
-------------------|---------|----------|---------|---------|-------------------