Skip to main content

Testing Overview

TalentG implements a comprehensive testing strategy covering unit tests, integration tests, end-to-end tests, and performance testing to ensure code quality and reliability.

Testing Strategy

Testing Pyramid

Test Coverage Goals

Test TypeCoverage TargetCurrent Coverage
Unit Tests95%92%
Integration Tests80%75%
E2E Tests60%55%
Overall85%81%

Unit Testing

Testing Framework

  • Jest: Test runner and assertion library
  • React Testing Library: Component testing utilities
  • @testing-library/jest-dom: Custom matchers

Component Testing

// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button Component', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('applies correct variant styles', () => {
    render(<Button variant="secondary">Secondary</Button>)
    const button = screen.getByRole('button')
    expect(button).toHaveClass('bg-gray-200')
  })

  it('is disabled when loading', () => {
    render(<Button loading>Loading</Button>)
    const button = screen.getByRole('button')
    expect(button).toBeDisabled()
    expect(screen.getByText('Loading...')).toBeInTheDocument()
  })
})

Hook Testing

// __tests__/hooks/use-auth.test.ts
import { renderHook, act } from '@testing-library/react'
import { useAuth } from '@/hooks/use-auth'
import { supabase } from '@/lib/supabase'

// Mock Supabase
jest.mock('@/lib/supabase', () => ({
  supabase: {
    auth: {
      onAuthStateChange: jest.fn(),
      getSession: jest.fn(),
    },
  },
}))

describe('useAuth Hook', () => {
  it('returns loading state initially', () => {
    const { result } = renderHook(() => useAuth())
    expect(result.current.loading).toBe(true)
    expect(result.current.user).toBe(null)
  })

  it('updates user when session changes', async () => {
    const mockUser = { id: '1', email: 'test@example.com' }
    const mockSession = { user: mockUser }
    
    supabase.auth.getSession.mockResolvedValue({
      data: { session: mockSession }
    })

    const { result } = renderHook(() => useAuth())
    
    await act(async () => {
      // Simulate auth state change
      const callback = supabase.auth.onAuthStateChange.mock.calls[0][0]
      callback('SIGNED_IN', mockSession)
    })

    expect(result.current.user).toEqual(mockUser)
    expect(result.current.loading).toBe(false)
  })
})

Utility Function Testing

// __tests__/lib/utils.test.ts
import { formatDate, validateEmail, calculateAge } from '@/lib/utils'

describe('Utility Functions', () => {
  describe('formatDate', () => {
    it('formats date correctly', () => {
      const date = new Date('2025-10-29T10:30:00Z')
      expect(formatDate(date)).toBe('Oct 29, 2025')
    })

    it('handles invalid date', () => {
      expect(formatDate(null)).toBe('Invalid Date')
    })
  })

  describe('validateEmail', () => {
    it('validates correct email', () => {
      expect(validateEmail('test@example.com')).toBe(true)
    })

    it('rejects invalid email', () => {
      expect(validateEmail('invalid-email')).toBe(false)
      expect(validateEmail('')).toBe(false)
    })
  })

  describe('calculateAge', () => {
    it('calculates age correctly', () => {
      const birthDate = new Date('1990-01-01')
      const currentDate = new Date('2025-10-29')
      expect(calculateAge(birthDate, currentDate)).toBe(35)
    })
  })
})

Integration Testing

API Testing

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

jest.mock('@/lib/supabase')

describe('/api/leads', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  describe('GET /api/leads', () => {
    it('returns leads for authenticated user', async () => {
      const mockLeads = [
        { id: '1', name: 'John Doe', email: 'john@example.com' },
        { id: '2', name: 'Jane Smith', email: 'jane@example.com' }
      ]

      supabase.from.mockReturnValue({
        select: jest.fn().mockReturnValue({
          order: jest.fn().mockReturnValue({
            range: jest.fn().mockResolvedValue({
              data: mockLeads,
              error: null,
              count: 2
            })
          })
        })
      })

      const { req, res } = createMocks({
        method: 'GET',
        headers: {
          authorization: 'Bearer valid-token'
        }
      })

      await handler(req, res)

      expect(res._getStatusCode()).toBe(200)
      expect(JSON.parse(res._getData())).toEqual({
        success: true,
        data: mockLeads,
        meta: { total: 2, page: 1, limit: 10 }
      })
    })

    it('returns 401 for unauthenticated request', async () => {
      supabase.auth.getSession.mockResolvedValue({
        data: { session: null }
      })

      const { req, res } = createMocks({
        method: 'GET'
      })

      await handler(req, res)

      expect(res._getStatusCode()).toBe(401)
      expect(JSON.parse(res._getData())).toEqual({
        success: false,
        error: { code: 'UNAUTHORIZED', message: 'Unauthorized' }
      })
    })
  })

  describe('POST /api/leads', () => {
    it('creates new lead', async () => {
      const newLead = {
        name: 'New Lead',
        email: 'new@example.com',
        phone: '123-456-7890'
      }

      supabase.from.mockReturnValue({
        insert: jest.fn().mockReturnValue({
          select: jest.fn().mockReturnValue({
            single: jest.fn().mockResolvedValue({
              data: { id: '3', ...newLead },
              error: null
            })
          })
        })
      })

      const { req, res } = createMocks({
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          authorization: 'Bearer valid-token'
        },
        body: newLead
      })

      await handler(req, res)

      expect(res._getStatusCode()).toBe(201)
      expect(JSON.parse(res._getData())).toEqual({
        success: true,
        data: { id: '3', ...newLead }
      })
    })
  })
})

Database Testing

// __tests__/lib/database.test.ts
import { createClient } from '@supabase/supabase-js'
import { testDatabase } from '@/lib/test-database'

describe('Database Operations', () => {
  let supabase: any

  beforeAll(async () => {
    supabase = createClient(
      process.env.TEST_SUPABASE_URL!,
      process.env.TEST_SUPABASE_ANON_KEY!
    )
  })

  beforeEach(async () => {
    await testDatabase.clean()
  })

  afterAll(async () => {
    await testDatabase.clean()
  })

  describe('Lead Operations', () => {
    it('creates lead with valid data', async () => {
      const leadData = {
        name: 'Test Lead',
        email: 'test@example.com',
        phone: '123-456-7890',
        company: 'Test Company'
      }

      const { data, error } = await supabase
        .from('leads')
        .insert(leadData)
        .select()
        .single()

      expect(error).toBeNull()
      expect(data).toMatchObject(leadData)
      expect(data.id).toBeDefined()
      expect(data.created_at).toBeDefined()
    })

    it('enforces RLS policies', async () => {
      // Test that users can only see their own leads
      const { data, error } = await supabase
        .from('leads')
        .select('*')

      expect(error).toBeNull()
      // Should only return leads assigned to the test user
      expect(data).toHaveLength(0) // No leads for test user
    })
  })
})

End-to-End Testing

Playwright Setup

// tests/e2e/setup.ts
import { test as base } from '@playwright/test'
import { LoginPage } from '../pages/login-page'
import { DashboardPage } from '../pages/dashboard-page'

type TestFixtures = {
  loginPage: LoginPage
  dashboardPage: DashboardPage
}

export const test = base.extend<TestFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page)
    await use(loginPage)
  },
  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page)
    await use(dashboardPage)
  },
})

export { expect } from '@playwright/test'

Page Object Model

// tests/pages/login-page.ts
import { Page, Locator } from '@playwright/test'

export class LoginPage {
  readonly page: Page
  readonly emailInput: Locator
  readonly passwordInput: Locator
  readonly loginButton: Locator
  readonly errorMessage: Locator

  constructor(page: Page) {
    this.page = page
    this.emailInput = page.locator('[data-testid="email-input"]')
    this.passwordInput = page.locator('[data-testid="password-input"]')
    this.loginButton = page.locator('[data-testid="login-button"]')
    this.errorMessage = page.locator('[data-testid="error-message"]')
  }

  async goto() {
    await this.page.goto('/login')
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email)
    await this.passwordInput.fill(password)
    await this.loginButton.click()
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent()
  }
}

E2E Test Cases

// tests/e2e/auth.spec.ts
import { test, expect } from '../setup'

test.describe('Authentication', () => {
  test('user can login with valid credentials', async ({ loginPage, page }) => {
    await loginPage.goto()
    await loginPage.login('test@example.com', 'password123')
    
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
  })

  test('user cannot login with invalid credentials', async ({ loginPage }) => {
    await loginPage.goto()
    await loginPage.login('invalid@example.com', 'wrongpassword')
    
    await expect(loginPage.errorMessage).toBeVisible()
    await expect(loginPage.errorMessage).toContainText('Invalid credentials')
  })

  test('user is redirected to login when not authenticated', async ({ page }) => {
    await page.goto('/dashboard')
    await expect(page).toHaveURL('/login')
  })
})
// tests/e2e/leads.spec.ts
import { test, expect } from '../setup'

test.describe('Lead Management', () => {
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto()
    await loginPage.login('test@example.com', 'password123')
  })

  test('user can create a new lead', async ({ page }) => {
    await page.goto('/leads')
    await page.click('[data-testid="create-lead-button"]')
    
    await page.fill('[data-testid="lead-name"]', 'John Doe')
    await page.fill('[data-testid="lead-email"]', 'john@example.com')
    await page.fill('[data-testid="lead-phone"]', '123-456-7890')
    await page.selectOption('[data-testid="lead-source"]', 'website')
    
    await page.click('[data-testid="save-lead-button"]')
    
    await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
    await expect(page.locator('text=John Doe')).toBeVisible()
  })

  test('user can update lead status', async ({ page }) => {
    await page.goto('/leads')
    
    // Find the first lead and click edit
    await page.click('[data-testid="lead-row"]:first-child [data-testid="edit-button"]')
    
    // Change status to 'contacted'
    await page.selectOption('[data-testid="lead-status"]', 'contacted')
    await page.click('[data-testid="save-button"]')
    
    await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
    await expect(page.locator('[data-testid="lead-status"]')).toHaveText('Contacted')
  })
})

Performance Testing

Load Testing

// tests/performance/load-test.ts
import { test, expect } from '@playwright/test'

test.describe('Performance Tests', () => {
  test('dashboard loads within acceptable time', async ({ page }) => {
    const startTime = Date.now()
    
    await page.goto('/dashboard')
    await page.waitForLoadState('networkidle')
    
    const loadTime = Date.now() - startTime
    expect(loadTime).toBeLessThan(3000) // 3 seconds
  })

  test('API response times are acceptable', async ({ request }) => {
    const startTime = Date.now()
    
    const response = await request.get('/api/leads')
    
    const responseTime = Date.now() - startTime
    expect(responseTime).toBeLessThan(1000) // 1 second
    expect(response.status()).toBe(200)
  })
})

Memory Testing

// tests/performance/memory-test.ts
import { test, expect } from '@playwright/test'

test.describe('Memory Tests', () => {
  test('no memory leaks in lead management', async ({ page }) => {
    await page.goto('/leads')
    
    // Perform multiple operations
    for (let i = 0; i < 10; i++) {
      await page.click('[data-testid="create-lead-button"]')
      await page.fill('[data-testid="lead-name"]', `Lead ${i}`)
      await page.click('[data-testid="save-lead-button"]')
      await page.click('[data-testid="close-modal"]')
    }
    
    // Check for memory usage
    const metrics = await page.evaluate(() => {
      return {
        memory: (performance as any).memory?.usedJSHeapSize || 0,
        nodes: document.querySelectorAll('*').length
      }
    })
    
    expect(metrics.memory).toBeLessThan(50 * 1024 * 1024) // 50MB
    expect(metrics.nodes).toBeLessThan(1000) // 1000 DOM nodes
  })
})

Test Configuration

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>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
}

module.exports = createJestConfig(customJestConfig)

Playwright Configuration

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/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'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Test Data Management

Test Fixtures

// tests/fixtures/test-data.ts
export const testUsers = {
  admin: {
    email: 'admin@test.com',
    password: 'password123',
    role: 'admin'
  },
  manager: {
    email: 'manager@test.com',
    password: 'password123',
    role: 'manager'
  },
  telecaller: {
    email: 'telecaller@test.com',
    password: 'password123',
    role: 'telecaller'
  }
}

export const testLeads = [
  {
    name: 'John Doe',
    email: 'john@example.com',
    phone: '123-456-7890',
    company: 'Acme Corp',
    status: 'new'
  },
  {
    name: 'Jane Smith',
    email: 'jane@example.com',
    phone: '098-765-4321',
    company: 'Tech Inc',
    status: 'contacted'
  }
]

Database Seeding

// tests/helpers/database-seed.ts
import { supabase } from '@/lib/supabase'
import { testUsers, testLeads } from '../fixtures/test-data'

export async function seedTestData() {
  // Create test users
  for (const user of Object.values(testUsers)) {
    const { data, error } = await supabase.auth.admin.createUser({
      email: user.email,
      password: user.password,
      user_metadata: { role: user.role }
    })
    
    if (error) throw error
  }

  // Create test leads
  const { error } = await supabase
    .from('leads')
    .insert(testLeads)
  
  if (error) throw error
}

export async function cleanupTestData() {
  // Clean up test data
  await supabase.from('leads').delete().neq('id', '00000000-0000-0000-0000-000000000000')
  // Note: User cleanup handled by Supabase auth admin
}

Continuous Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, staging]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Run unit tests
        run: pnpm test:unit
      
      - name: Run integration tests
        run: pnpm test:integration
        env:
          TEST_SUPABASE_URL: ${{ secrets.TEST_SUPABASE_URL }}
          TEST_SUPABASE_ANON_KEY: ${{ secrets.TEST_SUPABASE_ANON_KEY }}
      
      - name: Run E2E tests
        run: pnpm test:e2e
      
      - name: Upload coverage reports
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

Best Practices

Test Organization

  1. Group related tests using describe blocks
  2. Use descriptive test names that explain the expected behavior
  3. Follow AAA pattern: Arrange, Act, Assert
  4. Keep tests independent and isolated
  5. Use meaningful assertions with specific error messages

Test Data

  1. Use factories for creating test data
  2. Clean up after tests to prevent interference
  3. Use realistic data that matches production patterns
  4. Avoid hardcoded values in test assertions

Performance

  1. Mock external dependencies to speed up tests
  2. Use parallel execution where possible
  3. Optimize database operations in integration tests
  4. Monitor test execution time and optimize slow tests

Maintenance

  1. Update tests when requirements change
  2. Refactor tests to reduce duplication
  3. Review test coverage regularly
  4. Document complex test scenarios