Testing Philosophy
We follow a comprehensive testing strategy that balances coverage with development speed:
Unit Tests - Test individual functions and components in isolation
Integration Tests - Test how components work together
End-to-End Tests - Test complete user flows in a real browser
Follow the testing pyramid: Many unit tests, fewer integration tests, and selective E2E tests for critical paths.
Testing Stack
Our testing infrastructure uses modern, industry-standard tools:
Vitest - Fast unit test runner
React Testing Library - Component testing utilities
Playwright - End-to-end testing framework
MSW (Mock Service Worker) - API mocking
Unit Testing
Setup
Vitest configuration in vitest.config.ts:
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: [ './src/test/setup.ts' ],
coverage: {
provider: 'v8' ,
reporter: [ 'text' , 'json' , 'html' ],
exclude: [
'node_modules/' ,
'src/test/' ,
'**/*.config.ts' ,
'**/*.d.ts'
]
}
} ,
resolve: {
alias: {
'@' : path . resolve ( __dirname , './src' )
}
}
})
Test Setup File
Configure test environment in src/test/setup.ts:
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach , vi } from 'vitest'
// Cleanup after each test
afterEach (() => {
cleanup ()
})
// Mock window.matchMedia
Object . defineProperty ( window , 'matchMedia' , {
writable: true ,
value: vi . fn (). mockImplementation ( query => ({
matches: false ,
media: query ,
onchange: null ,
addListener: vi . fn (),
removeListener: vi . fn (),
addEventListener: vi . fn (),
removeEventListener: vi . fn (),
dispatchEvent: vi . fn (),
})),
})
Testing Components
Basic Component Test
Test component rendering and basic interactions: // Button.test.tsx
import { render , screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe , it , expect , vi } from 'vitest'
import { Button } from './Button'
describe ( 'Button' , () => {
it ( 'renders button with text' , () => {
render (< Button > Click me </ Button > )
expect ( screen . getByRole ( 'button' , { name: /click me/ i })). toBeInTheDocument ()
})
it ( 'calls onClick when clicked' , async () => {
const handleClick = vi . fn ()
const user = userEvent . setup ()
render (< Button onClick ={ handleClick }> Click me </ Button > )
await user . click ( screen . getByRole ( 'button' ))
expect ( handleClick ). toHaveBeenCalledTimes ( 1 )
})
it ( 'is disabled when disabled prop is true' , () => {
render (< Button disabled > Click me </ Button > )
expect ( screen . getByRole ( 'button' )). toBeDisabled ()
})
})
Testing with Props
Test different prop combinations: // Card.test.tsx
import { render , screen } from '@testing-library/react'
import { describe , it , expect } from 'vitest'
import { Card } from './Card'
describe ( 'Card' , () => {
it ( 'renders title and description' , () => {
render (
< Card title = "Test Title" description = "Test Description" />
)
expect ( screen . getByText ( 'Test Title' )). toBeInTheDocument ()
expect ( screen . getByText ( 'Test Description' )). toBeInTheDocument ()
})
it ( 'renders with variant styles' , () => {
const { container } = render (
< Card title = "Test" variant = "outlined" />
)
expect ( container . firstChild ). toHaveClass ( 'border' )
})
})
Testing Hooks
Test custom React hooks: // useCounter.test.ts
import { renderHook , act } from '@testing-library/react'
import { describe , it , expect } from 'vitest'
import { useCounter } from './useCounter'
describe ( 'useCounter' , () => {
it ( 'initializes with default value' , () => {
const { result } = renderHook (() => useCounter ())
expect ( result . current . count ). toBe ( 0 )
})
it ( 'increments counter' , () => {
const { result } = renderHook (() => useCounter ())
act (() => {
result . current . increment ()
})
expect ( result . current . count ). toBe ( 1 )
})
it ( 'resets counter' , () => {
const { result } = renderHook (() => useCounter ( 10 ))
act (() => {
result . current . increment ()
result . current . reset ()
})
expect ( result . current . count ). toBe ( 10 )
})
})
Testing Utilities
Create reusable test utilities in src/test/utils.tsx:
import { ReactElement } from 'react'
import { render , RenderOptions } from '@testing-library/react'
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
// Create a custom render function with providers
const AllTheProviders = ({ children } : { children : React . ReactNode }) => {
const queryClient = new QueryClient ({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return (
< QueryClientProvider client = { queryClient } >
< BrowserRouter >
{ children }
</ BrowserRouter >
</ QueryClientProvider >
)
}
const customRender = (
ui : ReactElement ,
options ?: Omit < RenderOptions , 'wrapper' >
) => render ( ui , { wrapper: AllTheProviders , ... options })
export * from '@testing-library/react'
export { customRender as render }
Integration Testing
API Mocking with MSW
Set up Mock Service Worker for API mocking:
// src/test/mocks/handlers.ts
import { http , HttpResponse } from 'msw'
export const handlers = [
// Mock GET request
http . get ( '/api/users/:id' , ({ params }) => {
const { id } = params
return HttpResponse . json ({
id ,
name: 'John Doe' ,
email: '[email protected] '
})
}),
// Mock POST request
http . post ( '/api/users' , async ({ request }) => {
const body = await request . json ()
return HttpResponse . json (
{ id: '123' , ... body },
{ status: 201 }
)
}),
// Mock error response
http . get ( '/api/error' , () => {
return HttpResponse . json (
{ message: 'Internal server error' },
{ status: 500 }
)
}),
]
// src/test/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer ( ... handlers )
Configure MSW in test setup:
// src/test/setup.ts
import { beforeAll , afterEach , afterAll } from 'vitest'
import { server } from './mocks/server'
// Start server before all tests
beforeAll (() => server . listen ({ onUnhandledRequest: 'error' }))
// Reset handlers after each test
afterEach (() => server . resetHandlers ())
// Clean up after all tests
afterAll (() => server . close ())
Testing API Integration
// UserProfile.test.tsx
import { render , screen , waitFor } from '@/test/utils'
import { describe , it , expect } from 'vitest'
import { server } from '@/test/mocks/server'
import { http , HttpResponse } from 'msw'
import { UserProfile } from './UserProfile'
describe ( 'UserProfile' , () => {
it ( 'loads and displays user data' , async () => {
render (< UserProfile userId = "1" />)
expect ( screen . getByText ( /loading/ i )). toBeInTheDocument ()
await waitFor (() => {
expect ( screen . getByText ( 'John Doe' )). toBeInTheDocument ()
expect ( screen . getByText ( '[email protected] ' )). toBeInTheDocument ()
})
})
it ( 'displays error message on failure' , async () => {
// Override default handler for this test
server . use (
http . get ( '/api/users/:id' , () => {
return HttpResponse . json (
{ message: 'User not found' },
{ status: 404 }
)
})
)
render (< UserProfile userId = "999" />)
await waitFor (() => {
expect ( screen . getByText ( /user not found/ i )). toBeInTheDocument ()
})
})
})
End-to-End Testing
Playwright Setup
Configure Playwright in 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' ,
} ,
projects: [
{
name: 'chromium' ,
use: { ... devices [ 'Desktop Chrome' ] },
},
{
name: 'firefox' ,
use: { ... devices [ 'Desktop Firefox' ] },
},
{
name: 'webkit' ,
use: { ... devices [ 'Desktop Safari' ] },
},
] ,
webServer: {
command: 'npm run dev' ,
url: 'http://localhost:3000' ,
reuseExistingServer: ! process . env . CI ,
} ,
})
Writing E2E Tests
Basic E2E Test
Page Object Model
// e2e/login.spec.ts
import { test , expect } from '@playwright/test'
test ( 'user can log in' , async ({ page }) => {
await page . goto ( '/login' )
await page . fill ( 'input[name="email"]' , '[email protected] ' )
await page . fill ( 'input[name="password"]' , 'password123' )
await page . click ( 'button[type="submit"]' )
await expect ( page ). toHaveURL ( '/dashboard' )
await expect ( page . getByText ( 'Welcome back' )). toBeVisible ()
})
Advanced E2E Patterns
Reuse authentication state across tests: // e2e/auth.setup.ts
import { test as setup } from '@playwright/test'
const authFile = 'playwright/.auth/user.json'
setup ( 'authenticate' , async ({ page }) => {
await page . goto ( '/login' )
await page . fill ( 'input[name="email"]' , '[email protected] ' )
await page . fill ( 'input[name="password"]' , 'password123' )
await page . click ( 'button[type="submit"]' )
await page . waitForURL ( '/dashboard' )
await page . context (). storageState ({ path: authFile })
})
// Use in tests
test . use ({ storageState: authFile })
Mock API responses in E2E tests: test ( 'displays mocked user data' , async ({ page }) => {
await page . route ( '/api/users/*' , async route => {
await route . fulfill ({
status: 200 ,
contentType: 'application/json' ,
body: JSON . stringify ({
id: '1' ,
name: 'Test User' ,
email: '[email protected] '
})
})
})
await page . goto ( '/profile' )
await expect ( page . getByText ( 'Test User' )). toBeVisible ()
})
Running Tests
# Run all unit tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run specific test file
npm run test Button.test.tsx
Test Coverage
Maintain high test coverage for critical code:
Coverage Goals:
Statements: > 80%
Branches: > 75%
Functions: > 80%
Lines: > 80%
Don’t obsess over 100% coverage. Focus on testing critical paths and complex logic.
Best Practices
Do’s
Write tests alongside your code
Test user behavior, not implementation details
Use descriptive test names
Keep tests isolated and independent
Mock external dependencies
Test error states and edge cases
Don’ts
Don’t test third-party libraries
Don’t test implementation details (CSS classes, state)
Don’t write flaky tests
Don’t skip tests without good reason
Don’t have test dependencies on each other
Testing Accessibility
import { render } from '@testing-library/react'
import { axe , toHaveNoViolations } from 'jest-axe'
import { Button } from './Button'
expect . extend ( toHaveNoViolations )
test ( 'Button has no accessibility violations' , async () => {
const { container } = render (< Button > Click me </ Button > )
const results = await axe ( container )
expect ( results ). toHaveNoViolations ()
})
Include accessibility testing in your component tests to catch a11y issues early.