Testing Guidelines

Testing standards, structure, and best practices

Testing Guidelines

Comprehensive testing guidelines including test structure, coverage requirements, and testing utilities.

Test Structure

Component Testing

// __tests__/components/product-card.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { ProductCard } from '@/components/products/product-card'
import { mockProduct } from '@/lib/test-utils/mocks'

describe('ProductCard', () => {
  it('renders product information correctly', () => {
    render(<ProductCard product={mockProduct} />)
    
    expect(screen.getByText(mockProduct.name)).toBeInTheDocument()
    expect(screen.getByText(mockProduct.sku)).toBeInTheDocument()
  })

  it('calls onEdit when edit button is clicked', () => {
    const onEdit = jest.fn()
    render(<ProductCard product={mockProduct} onEdit={onEdit} />)
    
    fireEvent.click(screen.getByRole('button', { name: /edit/i }))
    
    expect(onEdit).toHaveBeenCalledWith(mockProduct)
  })
})

Service Testing

// __tests__/services/product-service.test.ts
import { productService } from '@/lib/services/product'
import { mockProduct } from '@/lib/test-utils/mocks'

describe('ProductService', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  describe('createProduct', () => {
    it('creates a product successfully', async () => {
      const productData = {
        name: 'Test Product',
        sku: 'TEST-001',
        price: 29.99
      }

      const result = await productService.create(productData)

      expect(result.success).toBe(true)
      expect(result.data).toMatchObject(productData)
    })

    it('handles validation errors', async () => {
      const invalidData = { name: '', sku: '', price: -1 }

      const result = await productService.create(invalidData)

      expect(result.success).toBe(false)
      expect(result.error).toBeDefined()
    })
  })
})

API Route Testing

// __tests__/api/products.test.ts
import { createMocks } from 'node-mocks-http'
import handler from '@/app/api/products/route'

describe('/api/products', () => {
  it('GET returns products list', async () => {
    const { req, res } = createMocks({
      method: 'GET',
      query: { page: '1', limit: '10' }
    })

    await handler(req, res)

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

  it('POST creates new product', async () => {
    const productData = {
      name: 'New Product',
      sku: 'NP-001',
      price: 49.99
    }

    const { req, res } = createMocks({
      method: 'POST',
      body: productData
    })

    await handler(req, res)

    expect(res._getStatusCode()).toBe(201)
    const data = JSON.parse(res._getData())
    expect(data.data).toMatchObject(productData)
  })
})

Test Coverage Requirements

Minimum Coverage Targets

  • Unit Tests: All utility functions and business logic (90%+)
  • Component Tests: All React components (85%+)
  • Integration Tests: API routes and database operations (80%+)
  • E2E Tests: Critical user workflows (70%+)

Coverage Reports

# Generate coverage report
pnpm test:coverage

# View coverage in browser
pnpm test:coverage:open

Test Utilities

Mock Data

// lib/test-utils/mocks.ts
export const mockProduct: Product = {
  id: '123',
  name: 'Test Product',
  sku: 'TEST-001',
  price: 29.99,
  cost_price: 15.00,
  category_id: 'cat-123',
  is_active: true,
  created_at: '2024-01-01T00:00:00Z',
  updated_at: '2024-01-01T00:00:00Z'
}

export const mockInventory: Inventory = {
  id: '456',
  product_id: '123',
  warehouse_id: 'wh-123',
  quantity_on_hand: 100,
  quantity_allocated: 10,
  quantity_available: 90,
  reorder_point: 20,
  max_stock_level: 500,
  created_at: '2024-01-01T00:00:00Z',
  updated_at: '2024-01-01T00:00:00Z'
}

export const mockUser: User = {
  id: 'user-123',
  email: 'test@example.com',
  name: 'Test User',
  role: 'admin',
  is_active: true,
  created_at: '2024-01-01T00:00:00Z',
  updated_at: '2024-01-01T00:00:00Z'
}

Test Helpers

// lib/test-utils/helpers.ts
import { render, RenderOptions } from '@testing-library/react'
import { ReactElement } from 'react'
import { ThemeProvider } from '@/components/providers/theme'

const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
  return (
    <ThemeProvider>
      {children}
    </ThemeProvider>
  )
}

const customRender = (
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options })

export * from '@testing-library/react'
export { customRender as render }

Testing Commands

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests with coverage
pnpm test:coverage

# Run specific test file
pnpm test products.test.ts

# Run tests matching pattern
pnpm test --testNamePattern="create product"

# Run E2E tests
pnpm test:e2e

Best Practices

Test Organization

  • Group related tests using describe blocks
  • Use descriptive test names that explain what is being tested
  • Follow the Arrange-Act-Assert pattern
  • Keep tests focused on a single behavior

Mocking Guidelines

  • Mock external dependencies (APIs, databases)
  • Use real implementations for internal utilities when possible
  • Reset mocks between tests
  • Mock at the appropriate level (module vs function)

Async Testing

// Testing async operations
describe('async operations', () => {
  it('handles async data fetching', async () => {
    const promise = fetchUserData('123')
    
    await expect(promise).resolves.toMatchObject({
      id: '123',
      name: expect.any(String)
    })
  })

  it('handles async errors', async () => {
    const promise = fetchUserData('invalid-id')
    
    await expect(promise).rejects.toThrow('User not found')
  })
})

Testing Hooks

// Testing custom hooks
import { renderHook, act } from '@testing-library/react'
import { useProducts } from '@/hooks/use-products'

describe('useProducts', () => {
  it('fetches products on mount', async () => {
    const { result } = renderHook(() => useProducts({ page: 1 }))

    expect(result.current.loading).toBe(true)

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0))
    })

    expect(result.current.loading).toBe(false)
    expect(result.current.products).toHaveLength(10)
  })
})