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: 'test@example.com' },
})
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: 'test@example.com',
name: 'Test User',
},
})
await POST(req)
expect(res._getStatusCode()).toBe(201)
const data = JSON.parse(res._getData())
expect(data.user.email).toBe('test@example.com')
})
})
})
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
Test Organization
Group related tests using describe
Use clear test descriptions
Follow AAA pattern (Arrange, Act, Assert)
Keep tests focused and isolated
Component Testing
Test user interactions
Verify rendered content
Check component states
Test error scenarios
API Testing
Test request validation
Check response formats
Handle error cases
Mock external services
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: 'test@example.com',
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
-------------------|---------|----------|---------|---------|-------------------