Testing React Applications: From Unit Tests to E2E with Playwright | SoniNow Blog

Limited TimeLearn More

testingreactplaywrightviteste2e

Testing React Applications: From Unit Tests to E2E with Playwright

Published

2026-06-23

Read Time

4 mins

Testing React Applications: From Unit Tests to E2E with Playwright

A comprehensive testing strategy covers multiple layers: unit tests validate isolated logic, integration tests verify component interactions, and end-to-end tests confirm that real user workflows work in a browser. Here is how to build each layer for a React application.

Unit Testing Business Logic with Vitest

Vitest is the standard test runner for Vite-based React projects. Use it for pure functions, utilities, and custom hooks — code that transforms data without rendering a UI.

// utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency, slugify } from './format'

describe('formatCurrency', () => {
  it('formats USD with two decimal places', () => {
    expect(formatCurrency(29.99, 'USD')).toBe('$29.99')
  })

  it('handles zero', () => {
    expect(formatCurrency(0, 'USD')).toBe('$0.00')
  })

  it('formats large numbers with commas', () => {
    expect(formatCurrency(1500000, 'USD')).toBe('$1,500,000.00')
  })
})

describe('slugify', () => {
  it('converts title to lowercase kebab-case', () => {
    expect(slugify('Hello World')).toBe('hello-world')
  })

  it('strips special characters', () => {
    expect(slugify('What is Next.js?')).toBe('what-is-nextjs')
  })
})

Unit tests should be fast — they run in milliseconds. If a test takes more than 100ms, you are probably testing the wrong thing (like hitting a real API).

Component Testing with React Testing Library

Test components the way users interact with them. React Testing Library encourages testing behavior (what renders, what happens on click) rather than implementation details (internal state, prop names).

// components/__tests__/add-to-cart.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { AddToCart } from '../add-to-cart'

describe('AddToCart', () => {
  it('shows the current quantity', () => {
    render(<AddToCart productId="1" />)
    expect(screen.getByText('1')).toBeDefined()
  })

  it('increments quantity when + is clicked', () => {
    render(<AddToCart productId="1" />)
    fireEvent.click(screen.getByText('+'))
    expect(screen.getByText('2')).toBeDefined()
  })

  it('does not decrement below 1', () => {
    render(<AddToCart productId="1" />)
    fireEvent.click(screen.getByText('-'))
    expect(screen.getByText('1')).toBeDefined()
  })

  it('calls onAddToCart with productId and quantity', () => {
    const onAddToCart = vi.fn()
    render(<AddToCart productId="1" onAddToCart={onAddToCart} />)
    fireEvent.click(screen.getByText('Add to Cart'))
    expect(onAddToCart).toHaveBeenCalledWith('1', 1)
  })
})

Test user-visible behavior: what text appears, what happens on click, what error messages show. This makes tests resilient to refactoring — if you change internal implementation but the UI stays the same, the test still passes.

Integration Testing with MSW and Playwright Component Tests

Mock external APIs with MSW (Mock Service Worker) to test how your components handle real server responses, including loading states, errors, and edge cases.

// mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/products', () => {
    return HttpResponse.json([
      { id: '1', name: 'Widget', price: 29.99 },
      { id: '2', name: 'Gadget', price: 49.99 },
    ])
  }),

  http.get('/api/products/:id', ({ params }) => {
    if (params.id === 'error') {
      return new HttpResponse(null, { status: 500 })
    }
    return HttpResponse.json({ id: params.id, name: 'Test', price: 10.00 })
  }),
]
// integration/products.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { ProductsPage } from '@/app/products/page'

it('renders the product list from the API', async () => {
  render(<ProductsPage />)
  await waitFor(() => {
    expect(screen.getByText('Widget')).toBeDefined()
    expect(screen.getByText('Gadget')).toBeDefined()
  })
})

Integration tests validate that your components, hooks, and API layer work together correctly. They catch bugs that unit tests miss — wrong data shapes, missing error states, incorrect list rendering.

End-to-End Testing with Playwright

E2E tests run in a real browser and verify authentic user flows. Playwright is the fastest and most reliable option for React applications.

// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test'

test('user can complete the checkout flow', async ({ page }) => {
  await page.goto('/products')
  await page.click('[data-testid="add-to-cart-1"]')
  await page.click('[data-testid="cart-link"]')

  await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1')

  await page.click('text=Checkout')
  await page.fill('[name="email"]', '[email protected]')
  await page.fill('[name="card"]', '4242424242424242')
  await page.click('text=Pay Now')

  await expect(page.locator('text=Order confirmed')).toBeVisible()
})

Use data-testid attributes for selectors rather than fragile CSS classes. Run E2E tests in CI against a preview deployment or a Docker-based environment. A full E2E suite of 20-30 tests should complete within 5 minutes.

Testing Pyramid That Scales

        ╱  E2E  ╲         — 10-15 critical user flows
       ╱─────────╲
      ╱Integration╲       — Tests with MSW + component lib
     ╱─────────────╲
    ╱Unit + Component╲    — Pure functions, hooks, behaviors
   ╱───────────────────╲

Invest 70% of your testing effort in unit and component tests — they catch the most bugs with the least maintenance cost. Use E2E tests sparingly for critical paths only.

Building a robust testing suite requires the right tooling, patterns, and CI integration. At SoniNow, we set up comprehensive testing for React applications — from Vitest unit tests to Playwright E2E suites — integrated into CI pipelines that catch regressions before they reach production.

Ready to improve your test coverage? Contact SoniNow for a testing strategy consultation.