Unit testing strategies for React components and UI elements to ensure reliability and maintainability.

Unit Testing

Test individual components in isolation with mocked dependencies.

Tools & Frameworks

VitestJestTesting LibraryMSW

What to Test

  • Component rendering with different props
  • Event handler function calls
  • State changes and updates
  • Conditional rendering logic
  • Error boundary behavior

Integration Testing

Test component interactions and data flow between connected components.

Tools & Frameworks

Testing LibraryReact QueryContext Providers

What to Test

  • Parent-child component communication
  • Context provider state sharing
  • API data fetching and display
  • Form submission workflows
  • Modal and overlay interactions

Visual Testing

Catch visual regressions and ensure consistent UI appearance.

Tools & Frameworks

PlaywrightPercy

What to Test

  • Component visual states
  • Responsive design breakpoints
  • Theme and style variations
  • Animation and transition states
  • Cross-browser rendering

Button Component Test

Vitest + Testing Library

Testing button rendering, props, and click handlers

import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

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

  it('calls onClick when clicked', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('applies variant styles correctly', () => {
    render(<Button variant="primary">Primary</Button>)
    expect(screen.getByText('Primary')).toHaveClass('bg-purple-500')
  })
})

Form Validation Test

Vitest

Testing form input validation and error states

import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { ContactForm } from './ContactForm'

describe('ContactForm', () => {
  it('shows error for invalid email', async () => {
    render(<ContactForm />)

    const emailInput = screen.getByLabelText(/email/i)
    fireEvent.change(emailInput, { target: { value: 'invalid-email' } })
    fireEvent.blur(emailInput)

    await waitFor(() => {
      expect(screen.getByText(/valid email/i)).toBeInTheDocument()
    })
  })

  it('submits form with valid data', async () => {
    const onSubmit = vi.fn()
    render(<ContactForm onSubmit={onSubmit} />)

    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'John Doe' }
    })
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' }
    })

    fireEvent.click(screen.getByText(/submit/i))

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        name: 'John Doe',
        email: 'john@example.com'
      })
    })
  })
})

✅ Do

  • • Test behavior, not implementation
  • • Write descriptive test names
  • • Use data-testid for reliable selectors
  • • Mock external dependencies
  • • Test error states and edge cases

❌ Don't

  • • Test implementation details
  • • Use complex selectors that break easily
  • • Write tests that depend on each other
  • • Ignore accessibility in tests
  • • Skip testing error boundaries

💡 Tips

  • • Use screen reader queries (getByRole)
  • • Test with realistic data
  • • Include loading and error states
  • • Test keyboard navigation
  • • Mock time-dependent functionality

Vitest

Fast unit test runner

Recommended

Testing Library

User-centric testing utilities

Essential

Playwright

E2E & visual testing

Visual

MSW

API mocking for tests

Mocking
Statements
95%
Branches
90%
Functions
95%
Lines
95%

Vitest Config

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      threshold: {
        global: {
          statements: 95,
          branches: 90,
          functions: 95,
          lines: 95
        }
      }
    }
  }
})

Test Setup

// src/test/setup.ts
import '@testing-library/jest-dom'
import { server } from './mocks/server'

// Mock IntersectionObserver
global.IntersectionObserver = vi.fn(() => ({
  observe: vi.fn(),
  disconnect: vi.fn(),
  unobserve: vi.fn()
}))

// Setup MSW
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())