Skip to main content

๐Ÿงช 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

# Install Playwright with all browsers
pnpm add -D @playwright/test
pnpm exec playwright install

# Configure playwright.config.ts
// 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

// 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

// 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

// 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
// __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
// __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
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 โ†’