Testing
Testing strategies including unit tests, component tests, integration tests, and E2E testing
Testing
Comprehensive testing strategies to ensure code quality and reliability across unit, component, integration, and end-to-end testing.
Testing Setup
Jest Configuration
// jest.config.js
const nextJest = require('next/jest')
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/$1',
},
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
'!**/.next/**',
],
}
module.exports = createJestConfig(customJestConfig)
// jest.setup.js
import '@testing-library/jest-dom'
// Mock Next.js router
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}),
useSearchParams: () => new URLSearchParams(),
usePathname: () => '/',
}))
Unit Testing
Utility Functions
// __tests__/utils.test.ts
import { formatCurrency, formatNumber, calculateTax } from '@/lib/utils'
describe('Utility Functions', () => {
describe('formatCurrency', () => {
it('formats currency correctly', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56')
expect(formatCurrency(0)).toBe('$0.00')
expect(formatCurrency(-50)).toBe('-$50.00')
})
it('handles edge cases', () => {
expect(formatCurrency(null)).toBe('$0.00')
expect(formatCurrency(undefined)).toBe('$0.00')
})
})
describe('formatNumber', () => {
it('formats numbers with commas', () => {
expect(formatNumber(1234)).toBe('1,234')
expect(formatNumber(1234567)).toBe('1,234,567')
expect(formatNumber(0)).toBe('0')
})
})
describe('calculateTax', () => {
it('calculates tax correctly', () => {
expect(calculateTax(100, 0.08)).toBe(8)
expect(calculateTax(0, 0.08)).toBe(0)
})
})
})
Service Layer Testing
// __tests__/services/inventory.test.ts
import { getInventoryStats } from '@/lib/services/inventory'
import { createClient } from '@/lib/supabase/server'
// Mock Supabase client
jest.mock('@/lib/supabase/server')
const mockCreateClient = createClient as jest.MockedFunction<typeof createClient>
describe('Inventory Service', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('calculates inventory stats correctly', async () => {
const mockSupabase = {
from: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
eq: jest.fn().mockResolvedValue({
data: [
{
id: '1',
cost_price: 10,
inventory: [{ quantity_on_hand: 100, reorder_point: 20 }]
},
{
id: '2',
cost_price: 5,
inventory: [{ quantity_on_hand: 5, reorder_point: 10 }]
}
]
})
}
mockCreateClient.mockResolvedValue(mockSupabase as any)
const stats = await getInventoryStats()
expect(stats.totalProducts).toBe(2)
expect(stats.totalValue).toBe(1025) // (100 * 10) + (5 * 5)
expect(stats.lowStockItems).toBe(1)
expect(stats.outOfStockItems).toBe(0)
})
})
Component Testing
Basic Component Testing
// __tests__/components/ProductCard.test.tsx
import { render, screen } from '@testing-library/react'
import { ProductCard } from '@/components/products/product-card'
const mockProduct = {
id: '1',
name: 'Test Product',
sku: 'TEST-001',
price: 29.99,
stock: 100,
image_url: '/test-image.jpg'
}
describe('ProductCard', () => {
it('renders product information', () => {
render(<ProductCard product={mockProduct} />)
expect(screen.getByText('Test Product')).toBeInTheDocument()
expect(screen.getByText('TEST-001')).toBeInTheDocument()
expect(screen.getByText('$29.99')).toBeInTheDocument()
})
it('shows low stock warning', () => {
const lowStockProduct = { ...mockProduct, stock: 5 }
render(<ProductCard product={lowStockProduct} />)
expect(screen.getByText(/low stock/i)).toBeInTheDocument()
})
it('shows out of stock message', () => {
const outOfStockProduct = { ...mockProduct, stock: 0 }
render(<ProductCard product={outOfStockProduct} />)
expect(screen.getByText(/out of stock/i)).toBeInTheDocument()
})
})
Interactive Component Testing
// __tests__/components/ProductForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ProductForm } from '@/components/products/product-form'
describe('ProductForm', () => {
const mockOnSubmit = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
})
it('submits form with correct data', async () => {
const user = userEvent.setup()
render(<ProductForm onSubmit={mockOnSubmit} />)
await user.type(screen.getByLabelText(/name/i), 'New Product')
await user.type(screen.getByLabelText(/sku/i), 'NEW-001')
await user.type(screen.getByLabelText(/price/i), '49.99')
await user.click(screen.getByRole('button', { name: /save/i }))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
name: 'New Product',
sku: 'NEW-001',
price: 49.99,
})
})
})
it('shows validation errors', async () => {
const user = userEvent.setup()
render(<ProductForm onSubmit={mockOnSubmit} />)
await user.click(screen.getByRole('button', { name: /save/i }))
expect(screen.getByText(/name is required/i)).toBeInTheDocument()
expect(screen.getByText(/sku is required/i)).toBeInTheDocument()
expect(mockOnSubmit).not.toHaveBeenCalled()
})
})
Integration Testing
API Route Testing
// __tests__/api/products.test.ts
import { createMocks } from 'node-mocks-http'
import handler from '@/app/api/products/route'
// Mock database
jest.mock('@/lib/supabase/server')
describe('/api/products', () => {
it('returns products list', async () => {
const { req, res } = createMocks({
method: 'GET',
})
await handler(req, res)
expect(res._getStatusCode()).toBe(200)
const data = JSON.parse(res._getData())
expect(data).toHaveProperty('products')
expect(Array.isArray(data.products)).toBe(true)
})
it('creates new product', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Test Product',
sku: 'TEST-001',
price: 29.99,
},
})
await handler(req, res)
expect(res._getStatusCode()).toBe(201)
const data = JSON.parse(res._getData())
expect(data.product).toMatchObject({
name: 'Test Product',
sku: 'TEST-001',
price: 29.99,
})
})
it('validates required fields', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: '', // Invalid empty name
},
})
await handler(req, res)
expect(res._getStatusCode()).toBe(400)
expect(JSON.parse(res._getData())).toHaveProperty('error')
})
})
Database Integration Testing
// __tests__/integration/database.test.ts
import { createClient } from '@supabase/supabase-js'
import { getProducts, createProduct } from '@/lib/services/products'
// Use test database
const supabase = createClient(
process.env.TEST_SUPABASE_URL!,
process.env.TEST_SUPABASE_KEY!
)
describe('Database Integration', () => {
beforeEach(async () => {
// Clean up test data
await supabase.from('products').delete().neq('id', '')
})
it('creates and retrieves products', async () => {
const productData = {
name: 'Integration Test Product',
sku: 'INT-001',
price: 19.99,
}
const created = await createProduct(productData)
expect(created).toMatchObject(productData)
const products = await getProducts()
expect(products).toHaveLength(1)
expect(products[0]).toMatchObject(productData)
})
})
End-to-End Testing
Playwright Setup
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: {
command: 'npm run dev',
port: 3000,
},
})
E2E Test Examples
// e2e/inventory.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Inventory Management', () => {
test.beforeEach(async ({ page }) => {
// Login
await page.goto('/auth/login')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'password')
await page.click('[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
test('complete inventory workflow', async ({ page }) => {
// Navigate to inventory
await page.click('text=Inventory')
await expect(page).toHaveURL('/inventory')
// Add new product
await page.click('text=Add Product')
await page.fill('[name="name"]', 'E2E Test Product')
await page.fill('[name="sku"]', 'E2E-001')
await page.fill('[name="price"]', '29.99')
await page.click('[type="submit"]')
// Verify product appears in list
await expect(page.locator('text=E2E Test Product')).toBeVisible()
// Edit product
await page.locator('[data-testid="product-E2E-001"]').click()
await page.click('[data-testid="edit-button"]')
await page.fill('[name="name"]', 'Updated E2E Product')
await page.click('[type="submit"]')
// Verify update
await expect(page.locator('text=Updated E2E Product')).toBeVisible()
// Delete product
await page.click('[data-testid="delete-button"]')
await page.click('[data-testid="confirm-delete"]')
// Verify deletion
await expect(page.locator('text=Updated E2E Product')).toBeHidden()
})
test('search and filter functionality', async ({ page }) => {
await page.goto('/inventory')
// Test search
await page.fill('[placeholder="Search products..."]', 'laptop')
await expect(page.locator('[data-testid="product-card"]')).toHaveCount(2)
// Test category filter
await page.selectOption('[name="category"]', 'electronics')
await expect(page.locator('[data-testid="product-card"]')).toHaveCount(1)
// Clear filters
await page.click('[data-testid="clear-filters"]')
await expect(page.locator('[data-testid="product-card"]')).toHaveCount(10)
})
})
Testing Best Practices
Test Organization
- Group related tests in describe blocks
- Use descriptive test names
- Keep tests focused and independent
- Use beforeEach for common setup
Mocking Strategies
// Mock external dependencies
jest.mock('next/navigation')
jest.mock('@/lib/supabase/server')
// Mock only what you need
const mockPush = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({ push: mockPush }),
}))
Test Data Management
// __tests__/fixtures/products.ts
export const mockProducts = [
{
id: '1',
name: 'Laptop',
sku: 'LAP-001',
price: 999.99,
stock: 50,
},
{
id: '2',
name: 'Mouse',
sku: 'MOU-001',
price: 29.99,
stock: 100,
},
]
export const createMockProduct = (overrides = {}) => ({
id: '1',
name: 'Test Product',
sku: 'TEST-001',
price: 19.99,
stock: 10,
...overrides,
})
Coverage Goals
- Aim for 80%+ code coverage
- Focus on critical business logic
- Test edge cases and error scenarios
- Use coverage reports to identify gaps
Continuous Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:ci
- run: npm run test:e2e
- uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info