Skip to content

Test Strategy — Vitest, RTL, MSW

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/senior-qa/test-strategy.md
DescriptionNot specified

Source Content

Test Strategy — Vitest, RTL, MSW

Vitest Configuration

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, // no need to import describe/it/expect
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
exclude: [
'**/*.stories.tsx',
'**/types.ts',
'**/index.ts', // barrel files
],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
src/test/setup.ts
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach, beforeAll, afterAll } from 'vitest'
import { server } from './msw-server'
// RTL cleanup after each test
afterEach(() => cleanup())
// MSW server lifecycle
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Component Tests — RTL Patterns

Dumb components — render and assert

UserCard.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { axe, toHaveNoViolations } from 'jest-axe'
import { UserCard } from './UserCard'
expect.extend(toHaveNoViolations)
const mockUser = {
id: 'usr_01',
name: 'Sarah Okafor',
email: 'sarah@lawfirm.com',
role: 'admin' as const,
}
describe('UserCard', () => {
it('renders user information', () => {
render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />)
expect(screen.getByText('Sarah Okafor')).toBeInTheDocument()
expect(screen.getByText('sarah@lawfirm.com')).toBeInTheDocument()
})
it('calls onEdit when edit button is activated', async () => {
const onEdit = vi.fn()
render(<UserCard user={mockUser} onEdit={onEdit} onDelete={vi.fn()} />)
await userEvent.click(screen.getByRole('button', { name: /edit/i }))
expect(onEdit).toHaveBeenCalledWith('usr_01')
})
it('calls onEdit when Enter is pressed on the edit button', async () => {
const onEdit = vi.fn()
render(<UserCard user={mockUser} onEdit={onEdit} onDelete={vi.fn()} />)
screen.getByRole('button', { name: /edit/i }).focus()
await userEvent.keyboard('{Enter}')
expect(onEdit).toHaveBeenCalledWith('usr_01')
})
it('disables delete button while deleting', () => {
render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} isDeleting />)
expect(screen.getByRole('button', { name: /deleting/i })).toBeDisabled()
})
it('has no axe violations', async () => {
const { container } = render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />)
expect(await axe(container)).toHaveNoViolations()
})
})

Smart components — RTL + MSW integration test

CasesPage.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { server } from '../test/msw-server'
import { CasesPage } from './CasesPage'
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
mutations: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
)
}
describe('CasesPage', () => {
it('shows cases after loading', async () => {
renderWithProviders(<CasesPage />)
// Loading state
expect(screen.getByRole('status', { name: /loading/i })).toBeInTheDocument()
// Data loads
await waitFor(() => {
expect(screen.getByText('Smith vs Jones')).toBeInTheDocument()
})
})
it('shows error state when API fails', async () => {
server.use(
http.get('/api/cases', () => HttpResponse.error())
)
renderWithProviders(<CasesPage />)
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/failed to load/i)
})
})
it('shows empty state when no cases', async () => {
server.use(
http.get('/api/cases', () => HttpResponse.json([]))
)
renderWithProviders(<CasesPage />)
await waitFor(() => {
expect(screen.getByText(/no cases yet/i)).toBeInTheDocument()
})
})
})

Hook tests

useUsers.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { server } from '../test/msw-server'
import { useUsers } from './useUsers'
function wrapper({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
describe('useUsers', () => {
it('fetches and returns users', async () => {
const { result } = renderHook(() => useUsers({ role: 'all' }), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toHaveLength(2)
expect(result.current.data?.[0].name).toBe('Sarah Okafor')
})
it('handles API errors', async () => {
server.use(http.get('/api/users', () => HttpResponse.error()))
const { result } = renderHook(() => useUsers({ role: 'all' }), { wrapper })
await waitFor(() => expect(result.current.isError).toBe(true))
})
})

MSW Setup

src/test/handlers.ts
import { http, HttpResponse } from 'msw'
import { mockUsers, mockCases } from './fixtures'
export const handlers = [
http.get('/api/users', () => HttpResponse.json(mockUsers)),
http.get('/api/users/:id', ({ params }) => {
const user = mockUsers.find((u) => u.id === params.id)
if (!user) return HttpResponse.json({ error: 'Not found' }, { status: 404 })
return HttpResponse.json(user)
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 'usr_new', ...body }, { status: 201 })
}),
http.get('/api/cases', () => HttpResponse.json(mockCases)),
]
src/test/msw-server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
src/test/fixtures.ts
import type { User, Case } from '../api/types'
export const mockUsers: User[] = [
{
id: 'usr_01',
name: 'Sarah Okafor',
email: 'sarah@lawfirm.com',
role: 'admin',
createdAt: '2026-01-15T10:00:00Z',
},
{
id: 'usr_02',
name: 'Marcus Rivera',
email: 'marcus@lawfirm.com',
role: 'editor',
createdAt: '2026-02-20T14:30:00Z',
},
]

Zod Schema Tests

user.schema.test.ts
import { describe, it, expect } from 'vitest'
import { userSchema, createUserFormSchema, userFiltersSchema } from './user.schema'
describe('userSchema', () => {
it('accepts valid user data', () => {
const result = userSchema.safeParse({
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Sarah Okafor',
email: 'sarah@lawfirm.com',
role: 'admin',
createdAt: '2026-01-15T10:00:00Z',
})
expect(result.success).toBe(true)
})
it('rejects invalid email', () => {
const result = userSchema.safeParse({ email: 'not-an-email' })
expect(result.success).toBe(false)
expect(result.error?.issues[0].path).toContain('email')
})
it('rejects unknown roles', () => {
const result = userSchema.safeParse({ role: 'superadmin' })
expect(result.success).toBe(false)
})
})
describe('userFiltersSchema — URL param coercion', () => {
it('coerces page string to number', () => {
const result = userFiltersSchema.parse({ page: '3' })
expect(result.page).toBe(3)
expect(typeof result.page).toBe('number')
})
it('uses defaults for missing fields', () => {
const result = userFiltersSchema.parse({})
expect(result.q).toBe('')
expect(result.role).toBe('all')
expect(result.page).toBe(1)
expect(result.sort).toBe('name')
})
})