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 Type | Coverage Target | Current Coverage |
|---|---|---|
| Unit Tests | 95% | 92% |
| Integration Tests | 80% | 75% |
| E2E Tests | 60% | 55% |
| Overall | 85% | 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
Copy
// __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
Copy
// __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
Copy
// __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
Copy
// __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
Copy
// __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
Copy
// 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
Copy
// 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
Copy
// 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')
})
})
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
# .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
- Group related tests using
describeblocks - Use descriptive test names that explain the expected behavior
- Follow AAA pattern: Arrange, Act, Assert
- Keep tests independent and isolated
- Use meaningful assertions with specific error messages
Test Data
- Use factories for creating test data
- Clean up after tests to prevent interference
- Use realistic data that matches production patterns
- Avoid hardcoded values in test assertions
Performance
- Mock external dependencies to speed up tests
- Use parallel execution where possible
- Optimize database operations in integration tests
- Monitor test execution time and optimize slow tests
Maintenance
- Update tests when requirements change
- Refactor tests to reduce duplication
- Review test coverage regularly
- Document complex test scenarios