๐งช Comprehensive Testing Strategy
Testing Pyramid from Discovery Requirements
E2E (10%): Critical user journeys from discovery phaseIntegration (30%): API and service integration testsUnit (60%): Business logic and utility functions
Modern Testing Stack
Playwright
E2E testing with multi-browser support
Vitest
Lightning-fast unit testing
MSW
API mocking for integration tests
E2E Testing with Playwright
- Critical Path Tests
- Mobile Testing
- Cross-Browser Testing
1
Setup Playwright
Copy
# Install Playwright with all browsers
pnpm add -D @playwright/test
pnpm exec playwright install
# Configure playwright.config.ts
Copy
// 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',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
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'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
2
User Journey Tests
Copy
// e2e/critical-paths.spec.ts
import { test, expect } from '@playwright/test'
// From user journey mapping in discovery
test.describe('User Onboarding Flow', () => {
test('new user can complete full onboarding', async ({ page }) => {
// 1. Landing page
await page.goto('/')
await expect(page).toHaveTitle(/Welcome/)
// Visual regression test
await expect(page).toHaveScreenshot('landing-page.png')
// 2. Sign up
await page.click('[data-testid="signup-button"]')
await page.fill('[name="email"]', '[email protected]')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('[type="submit"]')
// 3. Email verification (mock in test env)
await page.goto('/verify-email?token=test-token')
// 4. Profile setup
await page.fill('[name="fullName"]', 'Test User')
await page.fill('[name="company"]', 'Test Corp')
await page.click('[data-testid="continue"]')
// 5. Onboarding tour
await expect(page.locator('[data-tour="step-1"]')).toBeVisible()
await page.click('[data-testid="skip-tour"]')
// 6. Dashboard
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Welcome, Test User')
// Performance metrics
const metrics = await page.evaluate(() => performance.getEntriesByType('navigation'))
expect(metrics[0].loadEventEnd).toBeLessThan(3000)
})
test('existing user can sign in', async ({ page }) => {
await page.goto('/signin')
await page.fill('[name="email"]', '[email protected]')
await page.fill('[name="password"]', 'password')
await page.click('[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
})
3
Accessibility Testing
Copy
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test.describe('Accessibility', () => {
test('homepage should have no violations', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
test('keyboard navigation works', async ({ page }) => {
await page.goto('/')
// Tab through interactive elements
await page.keyboard.press('Tab')
const firstFocused = await page.evaluate(() => document.activeElement?.tagName)
expect(firstFocused).toBeTruthy()
// Check skip to content link
await page.keyboard.press('Enter')
const mainContent = await page.locator('main')
await expect(mainContent).toBeFocused()
})
test('screen reader compatibility', async ({ page }) => {
await page.goto('/')
// Check ARIA labels
const buttons = await page.getByRole('button').all()
for (const button of buttons) {
const ariaLabel = await button.getAttribute('aria-label')
const text = await button.textContent()
expect(ariaLabel || text).toBeTruthy()
}
// Check heading hierarchy
const headings = await page.evaluate(() => {
return Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'))
.map(h => ({ level: parseInt(h.tagName[1]), text: h.textContent }))
})
// Verify proper heading structure
let previousLevel = 0
for (const heading of headings) {
expect(heading.level - previousLevel).toBeLessThanOrEqual(1)
previousLevel = heading.level
}
})
})
4
Performance Testing
Copy
// e2e/performance.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Performance Metrics', () => {
test('meets Core Web Vitals', async ({ page }) => {
await page.goto('/')
// Measure Core Web Vitals
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
let lcp, fid, cls
// Largest Contentful Paint
new PerformanceObserver((list) => {
const entries = list.getEntries()
lcp = entries[entries.length - 1].renderTime || entries[entries.length - 1].loadTime
}).observe({ entryTypes: ['largest-contentful-paint'] })
// First Input Delay (simulated)
addEventListener('click', (e) => {
fid = performance.now() - e.timeStamp
}, { once: true })
// Cumulative Layout Shift
let clsValue = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
cls = clsValue
}).observe({ entryTypes: ['layout-shift'] })
setTimeout(() => {
resolve({ lcp, fid, cls })
}, 5000)
})
})
// Assert against targets from discovery
expect(metrics.lcp).toBeLessThan(2500) // Good LCP
expect(metrics.cls).toBeLessThan(0.1) // Good CLS
})
test('bundle size check', async ({ page }) => {
const response = await page.goto('/')
const resources = await page.evaluate(() =>
performance.getEntriesByType('resource')
.filter(r => r.name.includes('.js'))
.reduce((acc, r) => acc + r.transferSize, 0)
)
// JavaScript budget from discovery
expect(resources).toBeLessThan(200 * 1024) // 200KB
})
})
Integration Testing
- API Testing
- Database Testing
- Service Testing
Copy
// __tests__/api/users.test.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createTestClient } from '@/tests/utils'
const server = setupServer(
http.post('/api/users', async ({ request }) => {
const body = await request.json()
// Validate request
if (!body.email || !body.password) {
return HttpResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
return HttpResponse.json({
id: '123',
email: body.email,
createdAt: new Date().toISOString()
}, { status: 201 })
})
)
beforeAll(() => server.listen())
afterAll(() => server.close())
describe('User API', () => {
const client = createTestClient()
describe('POST /api/users', () => {
it('creates user with valid data', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123!',
fullName: 'New User'
}
const response = await client.post('/api/users', userData)
expect(response.status).toBe(201)
expect(response.data).toHaveProperty('id')
expect(response.data.email).toBe(userData.email)
})
it('validates email format', async () => {
const response = await client.post('/api/users', {
email: 'invalid-email',
password: 'password'
})
expect(response.status).toBe(400)
expect(response.data.error).toContain('email')
})
it('enforces password requirements from discovery', async () => {
const response = await client.post('/api/users', {
email: '[email protected]',
password: '123' // Too short
})
expect(response.status).toBe(400)
expect(response.data.error).toContain('password')
})
})
describe('Rate limiting', () => {
it('enforces rate limits', async () => {
const requests = Array(100).fill(null).map(() =>
client.get('/api/users')
)
const responses = await Promise.all(requests)
const rateLimited = responses.filter(r => r.status === 429)
expect(rateLimited.length).toBeGreaterThan(0)
})
})
})
Unit Testing with Vitest
- Component Testing
- Hook Testing
- Utility Testing
Copy
// __tests__/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest'
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('handles click events', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('shows loading state', () => {
render(<Button loading>Submit</Button>)
expect(screen.getByTestId('spinner')).toBeInTheDocument()
expect(screen.getByText('Submit')).toHaveAttribute('disabled')
})
it('applies variant styles', () => {
const { rerender } = render(<Button variant="primary">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('bg-primary')
rerender(<Button variant="secondary">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('bg-secondary')
rerender(<Button variant="ghost">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('hover:bg-accent')
})
it('handles disabled state', () => {
const handleClick = vi.fn()
render(<Button disabled onClick={handleClick}>Disabled</Button>)
const button = screen.getByText('Disabled')
expect(button).toBeDisabled()
fireEvent.click(button)
expect(handleClick).not.toHaveBeenCalled()
})
})
Test Coverage & Quality
Coverage Requirements
Based on discovery phase requirements:
- Unit Tests: 80% coverage minimum
- Integration Tests: Critical paths covered
- E2E Tests: User journeys from discovery
- Performance Tests: Meet Core Web Vitals
- Security Tests: OWASP Top 10 covered
Copy
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'tests/',
'*.config.ts',
'**/*.d.ts',
'**/*.test.tsx',
'**/*.spec.tsx'
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
mockReset: true,
restoreMocks: true,
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
watchExclude: ['node_modules', 'dist', 'coverage']
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Next Step
With comprehensive testing in place, proceed to deployment strategies โ

