The React testing landscape has evolved significantly. Enzyme is gone, Jest faces competition from Vitest, and Server Components require entirely new testing patterns.
Vitest vs Jest
Vitest has emerged as a strong Jest alternative with better ESM support and faster execution:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
include: ['**/*.test.{ts,tsx}'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/']
}
}
})Choose Vitest when: New projects, ESM-first, fast iteration. Stay with Jest when: Large existing test suite, complex mocking needs.
Component Testing with React Testing Library
import { render, screen, userEvent } from '@testing-library/react'
import { CreatePostForm } from './CreatePostForm'
describe('CreatePostForm', () => {
it('submits form data correctly', async () => {
const onSubmit = vi.fn()
const user = userEvent.setup()
render(<CreatePostForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/title/i), 'Test Post')
await user.type(screen.getByLabelText(/content/i), 'Post content')
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(onSubmit).toHaveBeenCalledWith({
title: 'Test Post',
content: 'Post content'
})
})
it('shows validation errors', async () => {
const user = userEvent.setup()
render(<CreatePostForm onSubmit={vi.fn()} />)
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByText(/title is required/i)).toBeInTheDocument()
})
})Testing Server Components
Server Components require different testing patterns:
// Testing Server Components
import { render } from '@testing-library/react'
import { ProductList } from './ProductList'
// Mock the data fetching
vi.mock('@/lib/db', () => ({
getProducts: vi.fn(() => Promise.resolve([
{ id: '1', name: 'Product 1', price: 10 },
{ id: '2', name: 'Product 2', price: 20 }
]))
}))
test('renders product list', async () => {
// Server Components are async
const ProductListResolved = await ProductList()
const { getByText } = render(ProductListResolved)
expect(getByText('Product 1')).toBeInTheDocument()
expect(getByText('Product 2')).toBeInTheDocument()
})E2E Testing with Playwright
// tests/checkout.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Checkout Flow', () => {
test('complete purchase flow', async ({ page }) => {
await page.goto('/products')
await page.click('[data-testid="add-to-cart-1"]')
await page.goto('/cart')
await expect(page.locator('.cart-item')).toHaveCount(1)
await page.click('text=Checkout')
await page.fill('[name="email"]', '[email protected]')
await page.fill('[name="card"]', '4242424242424242')
await page.click('text=Pay')
await expect(page).toHaveURL(/\/order\/success/)
await expect(page.locator('h1')).toContainText('Thank you')
})
})The Testing Trophy
π E2E Tests (Few, critical paths)
ββββββββββββββββββββββββ
Integration Tests (Some, feature flows)
ββββββββββββββββββββββββββββββββββ
Unit Tests (Many, pure functions/utilities)
ββββββββββββββββββββββββββββββββββββββββββββ
Static Analysis (TypeScript, ESLint)
ββββββββββββββββββββββββββββββββββββββββββββββFocus your effort: More integration tests, fewer brittle unit tests. E2E for critical user journeys.
Conclusion
Modern React testing favors integration over isolation, Playwright for E2E, and Vitest for speed. The goal isn't coverage numbersβit's confidence that your application works correctly.
